skypro 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skypro/__init__.py +0 -0
- skypro/commands/__init__.py +0 -0
- skypro/commands/pull_elexon_imbalance/README.md +4 -0
- skypro/commands/pull_elexon_imbalance/__init__.py +0 -0
- skypro/commands/pull_elexon_imbalance/main.py +104 -0
- skypro/commands/pull_elexon_imbalance/utils.py +33 -0
- skypro/commands/report/README.md +24 -0
- skypro/commands/report/__init__.py +0 -0
- skypro/commands/report/config/__init__.py +0 -0
- skypro/commands/report/config/config.py +140 -0
- skypro/commands/report/main.py +547 -0
- skypro/commands/report/microgrid_flow_calcs.py +427 -0
- skypro/commands/report/plots.py +26 -0
- skypro/commands/report/rates.py +147 -0
- skypro/commands/report/readings.py +89 -0
- skypro/commands/report/warnings.py +62 -0
- skypro/commands/simulator/README.md +26 -0
- skypro/commands/simulator/__init__.py +0 -0
- skypro/commands/simulator/algorithms/__init__.py +0 -0
- skypro/commands/simulator/algorithms/lp/__init__.py +0 -0
- skypro/commands/simulator/algorithms/lp/optimiser.py +456 -0
- skypro/commands/simulator/algorithms/price_curve/__init__.py +0 -0
- skypro/commands/simulator/algorithms/price_curve/algo.py +320 -0
- skypro/commands/simulator/algorithms/price_curve/microgrid.py +36 -0
- skypro/commands/simulator/algorithms/price_curve/peak.py +225 -0
- skypro/commands/simulator/algorithms/price_curve/system_state.py +33 -0
- skypro/commands/simulator/algorithms/rate_management.py +103 -0
- skypro/commands/simulator/algorithms/utils.py +33 -0
- skypro/commands/simulator/cartesian.py +61 -0
- skypro/commands/simulator/config/__init__.py +14 -0
- skypro/commands/simulator/config/config.py +368 -0
- skypro/commands/simulator/config/curve.py +39 -0
- skypro/commands/simulator/config/parse_config.py +53 -0
- skypro/commands/simulator/main.py +728 -0
- skypro/commands/simulator/microgrid.py +41 -0
- skypro/commands/simulator/normalise_data.py +178 -0
- skypro/commands/simulator/profiler.py +102 -0
- skypro/commands/simulator/results.py +227 -0
- skypro/common/__init__.py +0 -0
- skypro/common/cli_utils/__init__.py +0 -0
- skypro/common/cli_utils/cli_utils.py +63 -0
- skypro/common/config/__init__.py +0 -0
- skypro/common/config/bill_match.py +16 -0
- skypro/common/config/data_source.py +101 -0
- skypro/common/config/data_source_csv.py +63 -0
- skypro/common/config/data_source_flows.py +45 -0
- skypro/common/config/dayed_period.py +82 -0
- skypro/common/config/path_field.py +35 -0
- skypro/common/config/rates_dataclasses.py +92 -0
- skypro/common/config/rates_parse_db.py +604 -0
- skypro/common/config/rates_parse_yaml.py +299 -0
- skypro/common/config/time_offset.py +26 -0
- skypro/common/config/utility.py +39 -0
- skypro/common/data/__init__.py +0 -0
- skypro/common/data/get_bess_readings.py +110 -0
- skypro/common/data/get_meter_readings.py +109 -0
- skypro/common/data/get_plot_meter_readings.py +125 -0
- skypro/common/data/get_profile.py +85 -0
- skypro/common/data/get_timeseries.py +128 -0
- skypro/common/data/utility.py +127 -0
- skypro/common/microgrid_analysis/__init__.py +0 -0
- skypro/common/microgrid_analysis/bill_match.py +136 -0
- skypro/common/microgrid_analysis/breakdown.py +185 -0
- skypro/common/microgrid_analysis/daily_gains.py +41 -0
- skypro/common/microgrid_analysis/output.py +371 -0
- skypro/common/notice/__init__.py +0 -0
- skypro/common/notice/notice.py +24 -0
- skypro/common/rate_utils/__init__.py +0 -0
- skypro/common/rate_utils/friendly_summary.py +60 -0
- skypro/common/rate_utils/osam.py +150 -0
- skypro/common/rate_utils/to_dfs.py +134 -0
- skypro/common/rates/README.md +41 -0
- skypro/common/rates/__init__.py +0 -0
- skypro/common/rates/rates.py +308 -0
- skypro/common/rates/supply_point.py +18 -0
- skypro/common/rates/time_varying_value.py +61 -0
- skypro/common/timeutils/__init__.py +1 -0
- skypro/common/timeutils/clock_time_period.py +56 -0
- skypro/common/timeutils/dayed_period.py +33 -0
- skypro/common/timeutils/days.py +38 -0
- skypro/common/timeutils/math.py +21 -0
- skypro/common/timeutils/math_wallclock.py +35 -0
- skypro/common/timeutils/month_str.py +41 -0
- skypro/common/timeutils/settlement_periods.py +14 -0
- skypro/common/timeutils/timeseries.py +26 -0
- skypro/main.py +134 -0
- skypro/reporting_webapp/README.md +21 -0
- skypro/reporting_webapp/__init__.py +0 -0
- skypro/reporting_webapp/example_config.yaml +5 -0
- skypro/reporting_webapp/main.py +357 -0
- skypro-1.2.0.dist-info/METADATA +109 -0
- skypro-1.2.0.dist-info/RECORD +95 -0
- skypro-1.2.0.dist-info/WHEEL +4 -0
- skypro-1.2.0.dist-info/entry_points.txt +5 -0
- skypro-1.2.0.dist-info/licenses/LICENSE +661 -0
skypro/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# pull-elexon-imbalance
|
|
2
|
+
|
|
3
|
+
This is a script to pulls a months worth of half-hourly imbalance price and volume data from Elexon and saves it to disk.
|
|
4
|
+
The data is saved in monthly CSV files in the directory defined by the `MARKET_DATA_DIR` variable in the environment configuration.
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from datetime import date, datetime, timedelta
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from skypro.common.cli_utils.cli_utils import get_user_ack_of_warning_or_exit, read_yaml_file
|
|
13
|
+
from skypro.common.data.utility import prepare_data_dir
|
|
14
|
+
|
|
15
|
+
from skypro.common.timeutils.month_str import get_first_and_last_date
|
|
16
|
+
from skypro.commands.pull_elexon_imbalance.utils import daterange, with_retries
|
|
17
|
+
|
|
18
|
+
ELEXON_API_MAX_RETRIES = 5
|
|
19
|
+
ELEXON_API_RETRY_DELAY = timedelta(seconds=1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pull_elexon_imbalance(month_str: str, env_file_path: str):
|
|
23
|
+
"""
|
|
24
|
+
Pulls a months worth of half-hourly imbalance price and volume data from Elexon and saves it to disk.
|
|
25
|
+
The data is saved in monthly CSV files in the directory defined by the MARKET_DATA_DIR in the environment configuration.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
start_date, end_date = get_first_and_last_date(month_str)
|
|
29
|
+
|
|
30
|
+
today = datetime.now().date()
|
|
31
|
+
if end_date > today:
|
|
32
|
+
end_date = today
|
|
33
|
+
get_user_ack_of_warning_or_exit(f"The month has not ended yet, so data will be incomplete after {end_date}")
|
|
34
|
+
|
|
35
|
+
env_config = read_yaml_file(env_file_path)
|
|
36
|
+
data_dir = env_config["vars"]["MARKET_DATA_DIR"]
|
|
37
|
+
|
|
38
|
+
df = _fetch_multiple_days(
|
|
39
|
+
start=start_date,
|
|
40
|
+
end=end_date,
|
|
41
|
+
fetch_func=_fetch_day,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
df["price"] = df["price"] / 10 # £/MW to p/kW
|
|
45
|
+
|
|
46
|
+
prices_file_path = prepare_data_dir(data_dir, "elexon", "imbalance_price", start_date)
|
|
47
|
+
volume_file_path = prepare_data_dir(data_dir, "elexon", "imbalance_volume", start_date)
|
|
48
|
+
|
|
49
|
+
logging.info(f"Saving pricing data to '{prices_file_path}'")
|
|
50
|
+
df[["spUTCTime", "spClockTime", "price"]].to_csv(prices_file_path, index=False)
|
|
51
|
+
logging.info(f"Saving volume data to '{volume_file_path}'")
|
|
52
|
+
df[["spUTCTime", "spClockTime", "volume"]].to_csv(volume_file_path, index=False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _fetch_multiple_days(start: date, end: date, fetch_func: Callable) -> pd.DataFrame:
|
|
56
|
+
"""
|
|
57
|
+
The elexon API pulls for a single day, so this function calls the elexon API repeatedly for each day and stacks up
|
|
58
|
+
the results.
|
|
59
|
+
"""
|
|
60
|
+
df = pd.DataFrame()
|
|
61
|
+
for day in daterange(start, end):
|
|
62
|
+
|
|
63
|
+
logging.info(f"Fetching imbalance data for '{str(day)}'...")
|
|
64
|
+
day_df = with_retries( # The Elexon API can be busy/unreliable at times so use retries to get past temporary failures
|
|
65
|
+
functools.partial(fetch_func, day),
|
|
66
|
+
ELEXON_API_MAX_RETRIES,
|
|
67
|
+
ELEXON_API_RETRY_DELAY
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
df = pd.concat([df, day_df])
|
|
71
|
+
|
|
72
|
+
# The values come through in the opposite order to what you'd expect
|
|
73
|
+
df = df.sort_values(by=["spUTCTime"], ignore_index=True)
|
|
74
|
+
|
|
75
|
+
return df
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _fetch_day(day: date) -> pd.DataFrame:
|
|
79
|
+
"""
|
|
80
|
+
Pulls a single days worth of imbalance data from Elexon.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
day_str = day.isoformat()
|
|
84
|
+
# Send a GET request to the API
|
|
85
|
+
response = requests.get(
|
|
86
|
+
url=f"https://data.elexon.co.uk/bmrs/api/v1/balancing/settlement/system-prices/{day_str}",
|
|
87
|
+
params={"format": "json"}
|
|
88
|
+
)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
|
|
91
|
+
json_data = json.load(StringIO(response.text))
|
|
92
|
+
day_df = pd.DataFrame.from_dict(json_data["data"])
|
|
93
|
+
|
|
94
|
+
day_df["startTime"] = pd.to_datetime(day_df["startTime"], utc=True)
|
|
95
|
+
day_df = day_df[["startTime", "systemSellPrice", "netImbalanceVolume"]]
|
|
96
|
+
day_df = day_df.rename(columns={
|
|
97
|
+
"startTime": "spUTCTime",
|
|
98
|
+
"systemSellPrice": "price",
|
|
99
|
+
"netImbalanceVolume": "volume"
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
day_df.insert(1, "spClockTime", day_df["spUTCTime"].dt.tz_convert("Europe/London"))
|
|
103
|
+
|
|
104
|
+
return day_df
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import date, timedelta
|
|
3
|
+
from time import sleep
|
|
4
|
+
from typing import Callable, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def with_retries(func: Callable, max_attempts: int, delay: timedelta) -> Any:
|
|
8
|
+
"""
|
|
9
|
+
This will call the given `func`, and retry up to `max_attempt` times if an exception is encountered.
|
|
10
|
+
"""
|
|
11
|
+
attempts = 0
|
|
12
|
+
while True:
|
|
13
|
+
try:
|
|
14
|
+
attempts += 1
|
|
15
|
+
result = func()
|
|
16
|
+
return result
|
|
17
|
+
except Exception as e:
|
|
18
|
+
logging.warning(f"Call to function failed: {e}")
|
|
19
|
+
if attempts < max_attempts:
|
|
20
|
+
logging.info(f"Retrying {attempts}/{max_attempts}")
|
|
21
|
+
sleep(delay.total_seconds())
|
|
22
|
+
continue
|
|
23
|
+
else:
|
|
24
|
+
logging.error("Max attempts reached.")
|
|
25
|
+
raise e
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def daterange(start: date, end: date):
|
|
29
|
+
"""
|
|
30
|
+
Yields every date between start and end.
|
|
31
|
+
"""
|
|
32
|
+
for n in range(int((end - start).days + 1)):
|
|
33
|
+
yield start + timedelta(days=n)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Skypro reporting
|
|
2
|
+
|
|
3
|
+
The reporting CLI tool collates historical metering actuals and rate/pricing information about a microgrid, and then analyses the data to produce a report which gives:
|
|
4
|
+
- Supplier invoice estimates, i.e. the costs associated with imports and revenues associated with exports.
|
|
5
|
+
- Summaries of energy flows in the microgrid, and their associated prices and costs.
|
|
6
|
+
- Solar statistics, including self-use.
|
|
7
|
+
- BESS statistics, including cycling and roundtrip efficiency.
|
|
8
|
+
|
|
9
|
+
The tool also reports on data inconsistencies by presenting 'Notices' to the user, which are graded according to how serious the issue is.
|
|
10
|
+
Some data inconsistencies are always going to be present because, for example, meters are not 100% accurate.
|
|
11
|
+
However, other data inconsistencies may be transient, for example, some metering data may be temporarily missing and so the reporting tool may need to make approximations.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
To run a microgrid report and plot the results: `skypro report -c <report-config-file> -m 2025-04 --plot`
|
|
17
|
+
|
|
18
|
+
See `skypro report -h` for help with command line options.
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
See the integration tests for an example configuration YAML file with inline comments: `src/tests/integration/fixtures/reporting/config.yaml`
|
|
23
|
+
|
|
24
|
+
Also, see the `skypro/commands/report/config/config.py` file for the configuration definition in code with comments.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from marshmallow_dataclass import dataclass
|
|
6
|
+
from skypro.common.config.bill_match import BillMatchLineItem
|
|
7
|
+
from skypro.common.config.path_field import PathField, PathType
|
|
8
|
+
from skypro.common.config.rates_dataclasses import Rates
|
|
9
|
+
from skypro.common.config.utility import field_with_opts
|
|
10
|
+
from skypro.common.config.data_source import MeterReadingDataSource, PlotMeterReadingDataSource, BessReadingDataSource
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
This file contains configuration schema that is used for reporting actuals on microgrid costs/revenues and battery performance.
|
|
14
|
+
The higher-level configuration structures are defined towards the end of the file, and the lower-level structures towards the top.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class MicrogridMeter:
|
|
20
|
+
"""
|
|
21
|
+
Configures the data source for a microgrid-level meter (e.g. an Acuvim meter)
|
|
22
|
+
"""
|
|
23
|
+
data_source: MeterReadingDataSource = field_with_opts(key="dataSource")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class MicrogridFeederMeter:
|
|
28
|
+
"""
|
|
29
|
+
Configures the data source for a microgrid-level feeder meter (e.g. an Acuvim meter), alongside the
|
|
30
|
+
ID that the feeder is assigned in the Flows database.
|
|
31
|
+
"""
|
|
32
|
+
data_source: MeterReadingDataSource = field_with_opts(key="dataSource")
|
|
33
|
+
feeder_flows_id: UUID = field_with_opts(key="feederId")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class MicrogridMeters:
|
|
38
|
+
"""
|
|
39
|
+
Holds configuration for all the microgrid-level meters on a site
|
|
40
|
+
"""
|
|
41
|
+
bess_inverter: MicrogridMeter = field_with_opts(key="bessInverter")
|
|
42
|
+
main_incomer: MicrogridMeter = field_with_opts(key="mainIncomer")
|
|
43
|
+
ev_charger: MicrogridMeter = field_with_opts(key="evCharger")
|
|
44
|
+
feeder_1: MicrogridFeederMeter = field_with_opts(key="feeder1")
|
|
45
|
+
feeder_2: MicrogridFeederMeter = field_with_opts(key="feeder2")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class PlotMeters:
|
|
50
|
+
"""
|
|
51
|
+
Configures the data source for plot-level meters (e.g. Emlite meters)
|
|
52
|
+
"""
|
|
53
|
+
data_source: PlotMeterReadingDataSource = field_with_opts(key="dataSource")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Metering:
|
|
58
|
+
"""
|
|
59
|
+
Holds configuration for both plot-level and microgrid-level meters
|
|
60
|
+
"""
|
|
61
|
+
plot_meters: Optional[PlotMeters] = field_with_opts(key="plotMeters")
|
|
62
|
+
microgrid_meters: MicrogridMeters = field_with_opts(key="microgridMeters")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Bess:
|
|
67
|
+
"""
|
|
68
|
+
Configures the size of a BESS in a microgrid, as well as the data source for BESS readings (which
|
|
69
|
+
hold things like state of energy kWh etc)
|
|
70
|
+
"""
|
|
71
|
+
energy_capacity: float = field_with_opts(key="energyCapacity")
|
|
72
|
+
data_source: BessReadingDataSource = field_with_opts(key="dataSource")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class GridConnection:
|
|
77
|
+
"""
|
|
78
|
+
Configures the size of the sites grid connection in kVA (this is used for calculating things like
|
|
79
|
+
the £/kVA/day costs).
|
|
80
|
+
"""
|
|
81
|
+
import_capacity: float = field_with_opts(key="importCapacity")
|
|
82
|
+
export_capacity: float = field_with_opts(key="exportCapacity")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class BillMatchImportOrExport:
|
|
87
|
+
"""
|
|
88
|
+
Configures a set of 'line items' that appear on a Suppliers invoice. Suppliers each have their own
|
|
89
|
+
way of formatting their invoices, and this allows us to express our costs/revenues in the same way as the
|
|
90
|
+
Suppliers, so we can easily compare. For example, some may have a line for "non commodity", and "duos", etc.
|
|
91
|
+
"""
|
|
92
|
+
line_items: Dict[str, BillMatchLineItem] = field_with_opts(key="lineItems")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class BillMatch:
|
|
97
|
+
"""
|
|
98
|
+
Optionally configures the import and export Supplier invoice formats, see above.
|
|
99
|
+
"""
|
|
100
|
+
import_direction: Optional[BillMatchImportOrExport] = field_with_opts(key="import")
|
|
101
|
+
export_direction: Optional[BillMatchImportOrExport] = field_with_opts(key="export")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class Reporting:
|
|
106
|
+
"""
|
|
107
|
+
Holds all configuration required to configure reporting of actual costs/revenues of a microgrid.
|
|
108
|
+
"""
|
|
109
|
+
metering: Metering
|
|
110
|
+
bess: Bess
|
|
111
|
+
grid_connection: GridConnection = field_with_opts(key="gridConnection")
|
|
112
|
+
bill_match: BillMatch = field_with_opts(key="billMatch")
|
|
113
|
+
profiles_save_dir: PathType = field_with_opts(key="profilesSaveDir") # Optionally save out the load and solar profiles to disk (useful for running simulations with actual solar and load profiles).
|
|
114
|
+
rates: Rates
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class Config:
|
|
119
|
+
reporting: Reporting
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_config(file_path: str, env_vars: dict) -> Config:
|
|
123
|
+
"""
|
|
124
|
+
Reads in the given file, and returns the parsed Config instance.
|
|
125
|
+
Variables may be specified to substitute into any file paths that are specified in the config - e.g. some paths
|
|
126
|
+
appear like "$SKYPRO_DIR/my_sims/blah.yaml" and the $SKYPRO_DIR will be substituted if there is a corresponding
|
|
127
|
+
entry in `env_vars`.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
# Read in the main config file
|
|
131
|
+
with open(file_path) as config_file:
|
|
132
|
+
# Here we parse the config file as YAML, which is a superset of JSON so allows us to parse JSON files as well
|
|
133
|
+
config_dict = yaml.safe_load(config_file)
|
|
134
|
+
|
|
135
|
+
# Set up the variables that are substituted into file paths
|
|
136
|
+
PathField.vars_for_substitution = env_vars
|
|
137
|
+
|
|
138
|
+
config = Config.Schema().load(config_dict)
|
|
139
|
+
|
|
140
|
+
return config
|