#!/usr/bin/env bash # SITER-Solar / KNELPerf — ONCOR-area real-time solar arbitrage decision. # # Question it answers, per interval: with X kW of solar surplus and a menu of # deferrable loads, is it better to (a) serve your own load, (b) export to # Base Power at the flat buyback, or (c) hold for later (battery)? # # Plan facts (Base Power, verified 2026-09-06): # import = $0.085/kWh flat (through Oct 2028) # export = $0.040/kWh flat (Base solar credit — NOT real-time wholesale) # Therefore: serving your own load ALWAYS beats exporting to Base # (8.5c avoided > 4c earned) whenever deferrable load exists. The ERCOT # real-time price (RTM, LZ_SOUTH for Pflugerville/Travis, ONCOR wires) # matters as a COUNTERFACTUAL: if you switched export to a real-time- # wholesale plan, when would exporting have beaten self-consumption? # # Price sources (first available wins; set via env or /etc/knelerf/ercot.env): # ERCOT_API_KEY official api.ercot.com public API (developer.ercot.com) # GRIDSTATUS_API_KEY api.gridstatus.io free tier # RTM_OVERRIDE manual $/MWh (testing / cache fallback) # RTM_CACHE_FILE last-known-good price file, refreshed when possible # Output: JSON verdict on stdout (consumed by the KNELPerf batch gate). set -euo pipefail IMPORT_RATE=${IMPORT_RATE:-0.085} # $/kWh, Base Power flat EXPORT_RATE=${EXPORT_RATE:-0.040} # $/kWh, Base Power flat buyback SETTLEMENT_POINT=${SETTLEMENT_POINT:-LZ_SOUTH} # Pflugerville/Travis Co. CACHE=${RTM_CACHE_FILE:-/tmp/knelerf-rtm-cache.json} now=$(date -u +%Y-%m-%dT%H:%M:%SZ) rtm="" fetch_gridstatus() { # needs GRIDSTATUS_API_KEY; returns $/MWh [ -n "${GRIDSTATUS_API_KEY:-}" ] || return 1 local url="https://api.gridstatus.io/v1/datasets/ercot_spp_real_time_15_min?limit=200" curl -sf -m 30 -H "x-api-key: $GRIDSTATUS_API_KEY" "$url" \ | jq -r --arg sp "$SETTLEMENT_POINT" \ '[.data[] | select(.settlement_point == $sp)][0].price // empty' } fetch_ercot() { # needs ERCOT_API_KEY (official) [ -n "${ERCOT_API_KEY:-}" ] || return 1 # official public API path; shape verified at registration time curl -sf -m 30 -H "Authorization: Bearer $ERCOT_API_KEY" -H "Accept: application/json" \ "https://api.ercot.com/api/public-api/np6-905-cd?deliveryDateFrom=$(date +%F)&settlementPoint=$SETTLEMENT_POINT&size=1" \ | jq -r '.data[-1].settlementPointPrice // empty' } [ -n "${RTM_OVERRIDE:-}" ] && rtm=$RTM_OVERRIDE [ -z "$rtm" ] && rtm=$(fetch_gridstatus || true) [ -z "$rtm" ] && rtm=$(fetch_ercot || true) src=api [ -n "${RTM_OVERRIDE:-}" ] && src=override if [ -z "$rtm" ] && [ -r "$CACHE" ]; then rtm=$(jq -r '.rtm_per_mwh // empty' "$CACHE"); src=cache fi if [ -z "$rtm" ]; then jq -n --arg now "$now" '{ts:$now, ok:false, error:"no price source available (set ERCOT_API_KEY or GRIDSTATUS_API_KEY; see docs/oncor-realtime-arbitrage.md)"}' exit 3 fi rtm_kwh=$(awk -v r="$rtm" 'BEGIN{printf "%.5f", r/1000}') # Decisions (per kWh of surplus): # serve_load_value = IMPORT_RATE (avoided purchase) # export_base = EXPORT_RATE (flat) # export_rtw_plan = rtm_kwh (counterfactual, real-time plan) verdict=$(awk -v i="$IMPORT_RATE" -v e="$EXPORT_RATE" -v r="$rtm_kwh" 'BEGIN{ best="serve_load"; v=i; if (r > v) {best="export_rtw_plan"; v=r} if (e > v) {best="export_base"; v=e} print best" "v}') read -r action value <<< "$verdict" # Batch-load gate (KNELPerf class-3): start deferrable compute while # surplus exists AND grid price would not make serving load later cheaper. jq -n --arg now "$now" --arg sp "$SETTLEMENT_POINT" --arg src "$src" \ --argjson rtm "$rtm" --argjson rtm_kwh "$rtm_kwh" \ --argjson import "$IMPORT_RATE" --argjson export "$EXPORT_RATE" \ --arg action "$action" --argjson value "$value" \ '{ts:$now, ok:true, settlement_point:$sp, source:$src, rtm_per_mwh:$rtm, rtm_per_kwh:$rtm_kwh, import_rate: $import, export_rate_base: $export, best_action: $action, best_value_per_kwh: $value, serve_load_wins: ($import > $export and $import > $rtm_kwh), rtw_would_beat_self_consume: ($rtm_kwh > $import), start_deferrable_load: ($import > $export)}' | tee /tmp/oncor-latest.json cp /tmp/oncor-latest.json "$CACHE" 2>/dev/null || true