#!/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"