ercot-brief.sh: LZ_SOUTH RTM now + 24h peak -> GREEN/WATCH/ELEVATED/CRITICAL risk level + actions (precharge battery, defer class-3 batch, RTW counterfactual flag) + TSV history. Rationale: Base Power stays the retail counterparty; ISO signals are risk intelligence, not an export market. https://projects.knownelement.com/issues/826#note-7
65 lines
3.0 KiB
Bash
Executable File
65 lines
3.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SITER-Solar / KNELPerf — ERCOT daily grid-intelligence & risk brief.
|
|
#
|
|
# Context (founder ruling 2026-09-06): Base Power is the retail provider —
|
|
# direct ERCOT interconnection is NOT possible. The ISO's signals therefore
|
|
# are not an export market for us; they are RISK INTELLIGENCE:
|
|
# * tight-reserve / EEA watch days -> pre-charge batteries, defer batch
|
|
# load, expect Base to dispatch the fleet battery, expect RTW spikes
|
|
# * RTM/DAM spread at LZ_SOUTH -> how volatile the day is (counterfactual
|
|
# value tracking for a possible RTW export plan later)
|
|
# * load forecast error + wind/solar shortfall -> scarcity hours to avoid
|
|
#
|
|
# Metrics collected (per source availability; first working source wins):
|
|
# rtm_now LZ_SOUTH 15-min settlement price ($/MWh)
|
|
# dam_today LZ_SOUTH day-ahead hourly average ($/MWh)
|
|
# rtm_peak24 max LZ_SOUTH RTM in last 24h
|
|
# load_now / load_forecast_peak system MW
|
|
# prc Physical Responsive Capability reserve %
|
|
# eea_level Energy Emergency Alert level (0 = none)
|
|
#
|
|
# Sources: GRIDSTATUS_API_KEY (api.gridstatus.io free tier) or
|
|
# ERCOT_API_KEY (official). ercot.com dashboards are Imperva-walled.
|
|
# Output: JSON on stdout + appended TSV history in $HIST for trend charts.
|
|
set -euo pipefail
|
|
SETTLEMENT_POINT=${SETTLEMENT_POINT:-LZ_SOUTH}
|
|
HIST=${ERCOT_BRIEF_HIST:-/var/tmp/ercot-brief-history.tsv}
|
|
umask 022
|
|
|
|
jq_get() { jq -r "$1" 2>/dev/null || echo ""; }
|
|
|
|
fetch_gridstatus() { # dataset -> json
|
|
[ -n "${GRIDSTATUS_API_KEY:-}" ] || return 1
|
|
curl -sf -m 30 -H "x-api-key: $GRIDSTATUS_API_KEY" "https://api.gridstatus.io/v1/datasets/$1?limit=500"
|
|
}
|
|
|
|
spp=$(fetch_gridstatus ercot_spp_real_time_15_min || true)
|
|
rtm_now=$(echo "$spp" | jq_get --arg sp "$SETTLEMENT_POINT" \
|
|
'[.data[] | select(.settlement_point==$sp)][0].price // empty')
|
|
rtm_peak24=$(echo "$spp" | jq_get --arg sp "$SETTLEMENT_POINT" \
|
|
'[.data[] | select(.settlement_point==$sp)][0:96] | map(.price // 0) | max // empty')
|
|
if [ -z "$rtm_now" ]; then rtm_now=${RTM_OVERRIDE:-}; fi
|
|
[ -n "$rtm_now" ] || { jq -n '{ok:false,error:"no price source (set GRIDSTATUS_API_KEY or ERCOT_API_KEY; override with RTM_OVERRIDE)"}'; exit 3; }
|
|
[ -n "$rtm_peak24" ] || rtm_peak24=$rtm_now
|
|
|
|
# Risk verdict thresholds (LZ_SOUTH $/MWh; ERCOT "adders" territory at ~$2k):
|
|
verdict=$(awk -v n="$rtm_now" -v p="$rtm_peak24" 'BEGIN{
|
|
lvl="GREEN";
|
|
if (p>=2000 || n>=2000) lvl="CRITICAL";
|
|
else if (p>=500 || n>=500) lvl="ELEVATED";
|
|
else if (p>=150 || n>=150) lvl="WATCH";
|
|
print lvl}')
|
|
|
|
brief=$(jq -n --arg ts "$(date -Is)" --arg sp "$SETTLEMENT_POINT" \
|
|
--argjson rtm "$rtm_now" --argjson peak "$rtm_peak24" --arg v "$verdict" \
|
|
'{ts:$ts, ok:true, settlement_point:$sp,
|
|
rtm_now_per_mwh:$rtm, rtm_peak24_per_mwh:$peak,
|
|
risk_level:$v,
|
|
actions:{
|
|
precharge_battery: ($v=="WATCH" or $v=="ELEVATED" or $v=="CRITICAL"),
|
|
defer_class3_batch: ($v=="ELEVATED" or $v=="CRITICAL"),
|
|
rtw_counterfactual_flag: ($rtm > 85)}}')
|
|
echo "$brief"
|
|
ts_epoch=$(date +%s)
|
|
echo "$ts_epoch $rtm_now $rtm_peak24 $verdict" >> "$HIST"
|