Source code for gridstatus.ercot

import pandas as pd

from gridstatus import utils
from gridstatus.base import FuelMix, GridStatus, ISOBase, NotSupported


[docs]class Ercot(ISOBase): """Electric Reliability Council of Texas (ERCOT)""" name = "Electric Reliability Council of Texas" iso_id = "ercot" default_timezone = "US/Central" status_homepage = "https://www.ercot.com/gridmktinfo/dashboards/gridconditions" BASE = "https://www.ercot.com/api/1/services/read/dashboards"
[docs] def get_status(self, date, verbose=False): """Returns status of grid""" if date != "latest": raise NotSupported() r = self._get_json(self.BASE + "/daily-prc.json", verbose=verbose) time = ( pd.to_datetime(r["current_condition"]["datetime"], unit="s") .tz_localize("UTC") .tz_convert(self.default_timezone) ) status = r["current_condition"]["state"] reserves = float(r["current_condition"]["prc_value"].replace(",", "")) if status == "normal": status = "Normal" notes = [r["current_condition"]["condition_note"]] return GridStatus( time=time, status=status, reserves=reserves, iso=self, notes=notes, )
[docs] def get_fuel_mix(self, date): if date == "latest": df = self.get_fuel_mix("today") currentHour = df.iloc[-1] mix_dict = {"Wind": currentHour["Wind"], "Solar": currentHour["Solar"]} return FuelMix(time=currentHour["Time"], mix=mix_dict, iso=self.name) elif utils.is_today(date): url = self.BASE + "/combine-wind-solar.json" r = self._get_json(url) # rows with nulls are forecasts df = pd.DataFrame(r["currentDay"]["data"]) df = df.dropna(subset=["actualSolar"]) df = self._handle_data( df, {"actualSolar": "Solar", "actualWind": "Wind"}, ) return df else: raise NotSupported()
[docs] def get_load(self, date, verbose=False): if date == "latest": d = self._get_load("currentDay").iloc[-1] return {"time": d["Time"], "load": d["Load"]} elif utils.is_today(date): return self._get_load("currentDay") else: raise NotSupported()
def _get_load(self, when): """Returns load for currentDay or previousDay""" # todo switch to https://www.ercot.com/content/cdr/html/20220810_actual_loads_of_forecast_zones.html # says supports last 5 days, appears to support last two weeks # df = pd.read_html("https://www.ercot.com/content/cdr/html/20220810_actual_loads_of_forecast_zones.html") # even more historical data. up to month back i think: https://www.ercot.com/mp/data-products/data-product-details?id=NP6-346-CD # hourly load archives: https://www.ercot.com/gridinfo/load/load_hist url = self.BASE + "/loadForecastVsActual.json" r = self._get_json(url) df = pd.DataFrame(r[when]["data"]) df = df.dropna(subset=["systemLoad"]) df = self._handle_data(df, {"systemLoad": "Load"}) return df
[docs] def get_supply(self, date, verbose=False): """Returns most recent data point for supply in MW Updates every 5 minutes """ if date == "latest": return self._latest_from_today(self.get_supply) elif utils.is_today(date): url = "https://www.ercot.com/api/1/services/read/dashboards/todays-outlook.json" r = self._get_json(url) date = pd.to_datetime(r["lastUpdated"][:10], format="%Y-%m-%d") # ignore last row since that corresponds to midnight following day data = pd.DataFrame(r["data"][:-1]) data["Time"] = pd.to_datetime( date.strftime("%Y-%m-%d") + " " + data["hourEnding"].astype(str).str.zfill(2) + ":" + data["interval"].astype(str).str.zfill(2), ).dt.tz_localize(self.default_timezone) data = data[data["forecast"] == 0] # only keep non forecast rows data = data[["Time", "capacity"]].rename( columns={"capacity": "Supply"}, ) return data else: raise NotSupported()
[docs] def get_load_forecast(self, date, verbose=False): if date != "today": raise NotSupported() # intrahour https://www.ercot.com/mp/data-products/data-product-details?id=NP3-562-CD # there are a few days of historical date for the forecast today = pd.Timestamp(pd.Timestamp.now(tz=self.default_timezone).date()) doc_url, publish_date = self._get_document( report_type_id=12311, date=today, constructed_name_contains="csv.zip", ) doc = pd.read_csv(doc_url, compression="zip") doc["Time"] = pd.to_datetime( doc["DeliveryDate"] + " " + (doc["HourEnding"].str.split(":").str[0].astype(int) - 1) .astype(str) .str.zfill(2) + ":00", ).dt.tz_localize(self.default_timezone) doc = doc.rename(columns={"SystemTotal": "Load Forecast"}) doc["Forecast Time"] = publish_date doc = doc[["Forecast Time", "Time", "Load Forecast"]] return doc
[docs] def get_rtm_spp(self, year): """Get Historical RTM Settlement Point Prices (SPPs) for each of the Hubs and Load Zones Arguments: year (int): year to get data for Source: https://www.ercot.com/mp/data-products/data-product-details?id=NP6-785-ER """ doc_url, date = self._get_document( 13061, constructed_name_contains=f"{year}.zip", verbose=True, ) x = utils.get_zip_file(doc_url) all_sheets = pd.read_excel(x, sheet_name=None) df = pd.concat(all_sheets.values()) return df
def _get_document( self, report_type_id, date=None, constructed_name_contains=None, verbose=False, ): """Get document for a given report type id and date. If multiple document published return the latest""" url = f"https://www.ercot.com/misapp/servlets/IceDocListJsonWS?reportTypeId={report_type_id}" docs = self._get_json(url)["ListDocsByRptTypeRes"]["DocumentList"] match = [] for d in docs: doc_date = pd.Timestamp(d["Document"]["PublishDate"]).tz_convert( self.default_timezone, ) # check do we need to check if same timezone? if date and doc_date.date() != date.date(): continue if ( constructed_name_contains and constructed_name_contains not in d["Document"]["ConstructedName"] ): continue match.append((doc_date, d["Document"]["DocID"])) if len(match) == 0: raise ValueError( f"No document found for {report_type_id} on {date}", ) doc = max(match, key=lambda x: x[0]) url = f"https://www.ercot.com/misdownload/servlets/mirDownload?doclookupId={doc[1]}" return url, doc[0] def _handle_data(self, df, columns): df["Time"] = ( pd.to_datetime(df["epoch"], unit="ms") .dt.tz_localize("UTC") .dt.tz_convert(self.default_timezone) ) cols_to_keep = ["Time"] + list(columns.keys()) return df[cols_to_keep].rename(columns=columns)
if __name__ == "__main__":
[docs] iso = Ercot()
iso.get_historical_rtm_spp(2020)