forked from electricitymaps/electricitymaps-contrib
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: upgrade CA-YT with event classes
Upgrade the parser to use the ProductionBreakdownList, ProductionMix, StorageMix, and ZoneKey classes. This should yield no functional change. Additionally: - Comply with Black's 88-column default line length limit. - Define global constants to replace magic values. - Eliminate a few unnecessary variables. Refs: electricitymaps#6011, electricitymaps#6062
- Loading branch information
Showing
1 changed file
with
75 additions
and
88 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()) |