Skip to content

Commit 94df187

Browse files
committed
adding ISO-NE Client
1 parent 8af83ea commit 94df187

File tree

6 files changed

+1144
-2
lines changed

6 files changed

+1144
-2
lines changed

isodart.py

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,140 @@ def handle_pjm(args):
638638
client.cleanup()
639639

640640

641+
def handle_isone(args):
642+
"""Handle ISO-NE-specific data download logic (updated for new ISONEClient)."""
643+
from lib.iso.isone import ISONEClient
644+
645+
logger.info(f"Processing ISO-NE data request: {args.data_type}")
646+
647+
client = ISONEClient()
648+
start = args.start
649+
end_excl = start + timedelta(days=args.duration)
650+
success = False
651+
652+
def _yyyymmdd(d: date) -> str:
653+
return d.strftime("%Y%m%d")
654+
655+
def _download_fivemin_lmp(location_id=None):
656+
"""Download 5-minute RT LMPs via REST (/fiveminutelmp). Saves one JSON per day."""
657+
out_paths = []
658+
for d in (start + timedelta(days=i) for i in range(args.duration)):
659+
day_str = _yyyymmdd(d)
660+
loc = int(location_id) if location_id is not None else 4000 # ISO-NE Internal Hub
661+
path = f"fiveminutelmp/day/{day_str}/location/{loc}"
662+
payload = client._request_json(path, authenticated=True)
663+
suffix = f"_loc{int(location_id)}" if location_id is not None else ""
664+
out_path = client.config.data_dir / f"fiveminutelmp_{day_str}{suffix}.json"
665+
client._save_json(payload, out_path)
666+
out_paths.append(out_path)
667+
return out_paths
668+
669+
def _download_hourly_rcp_final():
670+
"""Download hourly final regulation clearing prices via REST (/hourlyrcp/final)."""
671+
out_paths = []
672+
for d in (start + timedelta(days=i) for i in range(args.duration)):
673+
day_str = _yyyymmdd(d)
674+
path = f"hourlyrcp/final/day/{day_str}"
675+
payload = client._request_json(path, authenticated=True)
676+
out_path = client.config.data_dir / f"hourlyrcp_final_{day_str}.json"
677+
client._save_json(payload, out_path)
678+
out_paths.append(out_path)
679+
return out_paths
680+
681+
try:
682+
if args.data_type == "lmp":
683+
# LMP types: da_hourly, rt_5min
684+
lmp_type = getattr(args, "lmp_type", "da_hourly")
685+
686+
if lmp_type == "da_hourly":
687+
logger.info("Downloading ISO-NE Day-Ahead Hourly LMP (REST)...")
688+
paths = client.get_hourly_lmp(start, end_excl, market="da", report="final")
689+
success = bool(paths)
690+
691+
elif lmp_type == "rt_5min":
692+
logger.info("Downloading ISO-NE Real-Time 5-Minute LMP (REST)...")
693+
location_id = getattr(args, "location_id", None)
694+
paths = _download_fivemin_lmp(location_id=location_id)
695+
success = bool(paths)
696+
697+
else:
698+
logger.error(f"Invalid LMP type: {lmp_type}")
699+
return False
700+
701+
elif args.data_type == "ancillary":
702+
# Ancillary types: 5min_reg, hourly_reg, 5min_reserves, hourly_reserves
703+
anc_type = getattr(args, "anc_type", "5min_reg")
704+
705+
if anc_type == "5min_reg":
706+
logger.info("Downloading ISO-NE 5-Minute Regulation Clearing Prices (Final)...")
707+
paths = client.get_5min_regulation_prices(start, end_excl)
708+
success = bool(paths)
709+
710+
elif anc_type == "hourly_reg":
711+
logger.info("Downloading ISO-NE Hourly Regulation Clearing Prices (Final)...")
712+
paths = _download_hourly_rcp_final()
713+
success = bool(paths)
714+
715+
elif anc_type == "5min_reserves":
716+
# Closest supported feed in the new client: Real-Time Hourly Operating Reserve
717+
location_id = getattr(args, "location_id", 7000)
718+
logger.info(
719+
f"Downloading ISO-NE Real-Time Hourly Operating Reserve (location {location_id})..."
720+
)
721+
paths = client.get_real_time_hourly_operating_reserve(
722+
start, end_excl, location_id=location_id
723+
)
724+
success = bool(paths)
725+
726+
elif anc_type == "hourly_reserves":
727+
location_id = getattr(args, "location_id", 7000)
728+
logger.info(
729+
f"Downloading ISO-NE Day-Ahead Hourly Operating Reserve (location {location_id})..."
730+
)
731+
paths = client.get_day_ahead_hourly_operating_reserve(
732+
start, end_excl, location_id=location_id
733+
)
734+
success = bool(paths)
735+
736+
else:
737+
logger.error(f"Invalid ancillary type: {anc_type}")
738+
return False
739+
740+
elif args.data_type == "demand":
741+
# Demand types: 5min, hourly_da
742+
demand_type = getattr(args, "demand_type", "5min")
743+
744+
if demand_type == "5min":
745+
logger.info("Downloading ISO-NE 5-Minute System Demand (REST)...")
746+
paths = client.get_5min_system_demand(start, end_excl)
747+
success = bool(paths)
748+
749+
elif demand_type == "hourly_da":
750+
logger.info("Downloading ISO-NE Day-Ahead Hourly Demand (REST)...")
751+
paths = client.get_day_ahead_hourly_demand(start, end_excl)
752+
success = bool(paths)
753+
754+
else:
755+
logger.error(f"Invalid demand type: {demand_type}")
756+
return False
757+
758+
else:
759+
logger.error(f"Unknown ISO-NE data type: {args.data_type}")
760+
logger.info("Available types: lmp, ancillary, demand")
761+
return False
762+
763+
if success:
764+
logger.info("✅ ISO-NE data downloaded successfully.")
765+
else:
766+
logger.error("❌ Failed to download ISO-NE data.")
767+
768+
return success
769+
770+
except Exception as e:
771+
logger.error(f"Error processing ISO-NE data: {e}", exc_info=True)
772+
return False
773+
774+
641775
def handle_weather(args):
642776
"""Handle weather data download logic."""
643777
from lib.weather.client import WeatherClient
@@ -712,7 +846,7 @@ def main():
712846

713847
parser.add_argument(
714848
"--iso",
715-
choices=["caiso", "miso", "nyiso", "bpa", "spp", "pjm"],
849+
choices=["caiso", "miso", "nyiso", "bpa", "spp", "pjm", "isone"],
716850
help="Independent System Operator",
717851
)
718852

@@ -761,7 +895,7 @@ def main():
761895
"rt_5min",
762896
"rt_hourly",
763897
],
764-
help="For MISO and PJM LMP: type of LMP data to download",
898+
help="For MISO, PJM, and ISO-NE LMP: type of LMP data to download",
765899
)
766900

767901
parser.add_argument(
@@ -864,6 +998,24 @@ def main():
864998
help="For PJM Ancillary Services: type of AS data",
865999
)
8661000

1001+
parser.add_argument(
1002+
"--anc-type",
1003+
choices=["5min_reg", "hourly_reg", "5min_reserves", "hourly_reserves"],
1004+
help="For ISO-NE Ancillary Services: type of AS data",
1005+
)
1006+
1007+
parser.add_argument(
1008+
"--demand-type",
1009+
choices=["5min", "hourly_da"],
1010+
help="For ISO-NE Demand data: type of demand data",
1011+
)
1012+
1013+
parser.add_argument(
1014+
"--location-id",
1015+
type=int,
1016+
help="For ISO-NE location ID: Needed for full day 5min data",
1017+
)
1018+
8671019
parser.add_argument(
8681020
"--include-solar",
8691021
action="store_true",
@@ -927,6 +1079,7 @@ def main():
9271079
"bpa": handle_bpa,
9281080
"spp": handle_spp,
9291081
"pjm": handle_pjm,
1082+
"isone": handle_isone,
9301083
}
9311084

9321085
handler = handlers.get(args.iso)

0 commit comments

Comments
 (0)