diff --git a/parsers/CA_YT.py b/parsers/CA_YT.py index 077ef6bdc0..74ebffbf69 100644 --- a/parsers/CA_YT.py +++ b/parsers/CA_YT.py @@ -1,128 +1,115 @@ #!/usr/bin/env python3 - from datetime import datetime from logging import Logger, getLogger +from typing import Any +from zoneinfo import ZoneInfo -import arrow from bs4 import BeautifulSoup from requests import Session -timezone = "America/Whitehorse" +from electricitymap.contrib.config import ZoneKey +from electricitymap.contrib.lib.models.event_lists import ProductionBreakdownList +from electricitymap.contrib.lib.models.events import ProductionMix, StorageMix +from parsers.lib.exceptions import ParserException + +SOURCE = "www.yukonenergy.ca" +TIMEZONE = ZoneInfo("America/Whitehorse") +URL = "http://www.yukonenergy.ca/consumption/chart_current.php?chart=current&width=420" +ZONE_KEY = ZoneKey("CA-YT") def fetch_production( - zone_key: str = "CA-YT", + zone_key: ZoneKey = ZONE_KEY, session: Session | None = None, target_datetime: datetime | None = None, logger: Logger = getLogger(__name__), -) -> dict: - """Requests the last known production mix (in MW) of a given region.""" - +) -> list[dict[str, Any]]: """ + Requests the last known production mix (in MW) of a given region. + We are using Yukon Energy's data from http://www.yukonenergy.ca/energy-in-yukon/electricity-101/current-energy-consumption Generation in Yukon is done with hydro, diesel oil, and LNG. There are two companies, Yukon Energy and ATCO aka Yukon Electric aka YECL. - Yukon Energy does most of the generation and feeds into Yukon's grid. - ATCO does operations, billing, and generation in some of the off-grid communities. + Yukon Energy does most of the generation and feeds into Yukon's grid. ATCO + does operations, billing, and generation in some of the off-grid + communities. See schema of the grid at http://www.atcoelectricyukon.com/About-Us/ - Per https://en.wikipedia.org/wiki/Yukon#Municipalities_by_population - of total population 35874 (2016 census), 28238 are in municipalities - that are connected to the grid - that is 78.7%. + Per https://en.wikipedia.org/wiki/Yukon#Municipalities_by_population of + total population 35874 (2016 census), 28238 are in municipalities that are + connected to the grid - that is 78.7%. - Off-grid generation is with diesel generators, this is not reported online as of 2017-06-23 - and is not included in this calculation. + Off-grid generation is with diesel generators, this is not reported online + as of 2017-06-23 and is not included in this calculation. - Yukon Energy reports only "hydro" and "thermal" generation. - Per http://www.yukonenergy.ca/ask-janet/lng-and-boil-off-gas, - in 2016 the thermal generation was about 50% diesel and 50% LNG. - But since Yukon Energy doesn't break it down on their website, - we return all thermal as "unknown". + Yukon Energy reports only "hydro" and "thermal" generation. Per + http://www.yukonenergy.ca/ask-janet/lng-and-boil-off-gas, in 2016 the + thermal generation was about 50% diesel and 50% LNG. But since Yukon Energy + doesn't break it down on their website, we return all thermal as "unknown". Per https://en.wikipedia.org/wiki/List_of_generating_stations_in_Yukon - Yukon Energy operates about 98% of Yukon's hydro capacity, the only exception is - the small 1.3 MW Fish Lake dam operated by ATCO/Yukon Electrical. - That's small enough to not matter, I think. + Yukon Energy operates about 98% of Yukon's hydro capacity, the only + exception is the small 1.3 MW Fish Lake dam operated by ATCO/Yukon + Electrical. That's small enough to not matter, I think. - There is also a small 0.81 MW wind farm, its current generation is not available. + There is also a small 0.81 MW wind farm, its current generation is not + available. """ - if target_datetime: - raise NotImplementedError("This parser is not yet able to parse past dates") - requests_obj = session or Session() - - url = "http://www.yukonenergy.ca/consumption/chart_current.php?chart=current&width=420" - response = requests_obj.get(url) - - soup = BeautifulSoup(response.text, "html.parser") - - def find_div_by_class(soup_obj, cls): - return soup_obj.find("div", attrs={"class": cls}) - - def parse_mw(text): + def _parse_mw(text): + """ + Extract the power value from the source's HTML text content. The text + is formatted as, e.g., "37.69 MW - hydro". + """ try: - return float(text[: text.index("MW")]) + return float(text[: text.index(" MW")]) except ValueError: return 0 - # date is specified like "Thursday, June 22, 2017" - source_date = find_div_by_class(soup, "current_date").text - - # time is specified like "11:55 pm" or "2:25 am" - source_time = find_div_by_class(soup, "current_time").text - datetime_text = f"{source_date} {source_time}" - datetime_arrow = arrow.get(datetime_text, "dddd, MMMM D, YYYY h:mm A") - datetime_datetime = arrow.get(datetime_arrow.datetime, timezone).datetime - - # generation is specified like "37.69 MW - hydro" - hydro_div = find_div_by_class(soup, "load_hydro") - hydro_text = hydro_div.div.text - hydro_generation = parse_mw(hydro_text) - - hydro_cap_div = find_div_by_class(soup, "avail_hydro") - if hydro_cap_div: - hydro_cap_text = hydro_cap_div.div.text - hydro_capacity = parse_mw(hydro_cap_text) - else: - # hydro capacity is not provided when thermal is used - hydro_capacity = None - - thermal_div = find_div_by_class(soup, "load_thermal") - if thermal_div.div: - thermal_text = thermal_div.div.text - thermal_generation = parse_mw(thermal_text) - else: - # thermal is not always used and when it's not used, it's not specified in HTML - thermal_generation = 0 - - data = { - "datetime": datetime_datetime, - "zoneKey": zone_key, - "production": { - "unknown": thermal_generation, - "hydro": hydro_generation, - # specify some sources that aren't present in Yukon as zero, - # this allows the analyzer to better estimate CO2eq - "coal": 0, - "nuclear": 0, - "geothermal": 0, - }, - "storage": {}, - "source": "www.yukonenergy.ca", - } - - if hydro_capacity: - data.update({"capacity": {"hydro": hydro_capacity}}) - - return data + if zone_key != ZONE_KEY: + raise ParserException("CA_YT.py", "Cannot parse zone '{zone_key}'", zone_key) + if target_datetime: + raise ParserException("CA_YT.py", "Unable to fetch historical data", zone_key) + + session = session or Session() + soup = BeautifulSoup(session.get(URL).text, "html.parser") + + # Extract the relevant HTML data. + # The date is formatted as, e.g., "Thursday, June 22, 2017". + date = soup.find("div", class_="current_date").text + # The time is formatted as, e.g., "11:55 pm" or "2:25 am". + time = soup.find("div", class_="current_time").text + # Note: hydro capacity is not provided when thermal is in use. + hydro_capacity = soup.find("div", class_="avail_hydro") + thermal = soup.find("div", class_="load_thermal").div + + production_breakdowns = ProductionBreakdownList(logger=logger) + production_breakdowns.append( + datetime=datetime.strptime(f"{date} {time}", "%A, %B %d, %Y %I:%M %p").replace( + tzinfo=TIMEZONE + ), + production=ProductionMix( + coal=0, + geothermal=0, + hydro=_parse_mw(soup.find("div", class_="load_hydro").div.text), + nuclear=0, + unknown=_parse_mw(thermal.text) if thermal else 0, + ), + source=SOURCE, + storage=StorageMix( + hydro=_parse_mw(hydro_capacity.div.text) if hydro_capacity else None + ), + zoneKey=ZONE_KEY, + ) + return production_breakdowns.to_list() if __name__ == "__main__": - """Main method, never used by the Electricity Map backend, but handy for testing.""" + # Never used by the Electricity Map backend, but handy for testing. print("fetch_production() ->") print(fetch_production())