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.
Files changed (95) hide show
  1. skypro/__init__.py +0 -0
  2. skypro/commands/__init__.py +0 -0
  3. skypro/commands/pull_elexon_imbalance/README.md +4 -0
  4. skypro/commands/pull_elexon_imbalance/__init__.py +0 -0
  5. skypro/commands/pull_elexon_imbalance/main.py +104 -0
  6. skypro/commands/pull_elexon_imbalance/utils.py +33 -0
  7. skypro/commands/report/README.md +24 -0
  8. skypro/commands/report/__init__.py +0 -0
  9. skypro/commands/report/config/__init__.py +0 -0
  10. skypro/commands/report/config/config.py +140 -0
  11. skypro/commands/report/main.py +547 -0
  12. skypro/commands/report/microgrid_flow_calcs.py +427 -0
  13. skypro/commands/report/plots.py +26 -0
  14. skypro/commands/report/rates.py +147 -0
  15. skypro/commands/report/readings.py +89 -0
  16. skypro/commands/report/warnings.py +62 -0
  17. skypro/commands/simulator/README.md +26 -0
  18. skypro/commands/simulator/__init__.py +0 -0
  19. skypro/commands/simulator/algorithms/__init__.py +0 -0
  20. skypro/commands/simulator/algorithms/lp/__init__.py +0 -0
  21. skypro/commands/simulator/algorithms/lp/optimiser.py +456 -0
  22. skypro/commands/simulator/algorithms/price_curve/__init__.py +0 -0
  23. skypro/commands/simulator/algorithms/price_curve/algo.py +320 -0
  24. skypro/commands/simulator/algorithms/price_curve/microgrid.py +36 -0
  25. skypro/commands/simulator/algorithms/price_curve/peak.py +225 -0
  26. skypro/commands/simulator/algorithms/price_curve/system_state.py +33 -0
  27. skypro/commands/simulator/algorithms/rate_management.py +103 -0
  28. skypro/commands/simulator/algorithms/utils.py +33 -0
  29. skypro/commands/simulator/cartesian.py +61 -0
  30. skypro/commands/simulator/config/__init__.py +14 -0
  31. skypro/commands/simulator/config/config.py +368 -0
  32. skypro/commands/simulator/config/curve.py +39 -0
  33. skypro/commands/simulator/config/parse_config.py +53 -0
  34. skypro/commands/simulator/main.py +728 -0
  35. skypro/commands/simulator/microgrid.py +41 -0
  36. skypro/commands/simulator/normalise_data.py +178 -0
  37. skypro/commands/simulator/profiler.py +102 -0
  38. skypro/commands/simulator/results.py +227 -0
  39. skypro/common/__init__.py +0 -0
  40. skypro/common/cli_utils/__init__.py +0 -0
  41. skypro/common/cli_utils/cli_utils.py +63 -0
  42. skypro/common/config/__init__.py +0 -0
  43. skypro/common/config/bill_match.py +16 -0
  44. skypro/common/config/data_source.py +101 -0
  45. skypro/common/config/data_source_csv.py +63 -0
  46. skypro/common/config/data_source_flows.py +45 -0
  47. skypro/common/config/dayed_period.py +82 -0
  48. skypro/common/config/path_field.py +35 -0
  49. skypro/common/config/rates_dataclasses.py +92 -0
  50. skypro/common/config/rates_parse_db.py +604 -0
  51. skypro/common/config/rates_parse_yaml.py +299 -0
  52. skypro/common/config/time_offset.py +26 -0
  53. skypro/common/config/utility.py +39 -0
  54. skypro/common/data/__init__.py +0 -0
  55. skypro/common/data/get_bess_readings.py +110 -0
  56. skypro/common/data/get_meter_readings.py +109 -0
  57. skypro/common/data/get_plot_meter_readings.py +125 -0
  58. skypro/common/data/get_profile.py +85 -0
  59. skypro/common/data/get_timeseries.py +128 -0
  60. skypro/common/data/utility.py +127 -0
  61. skypro/common/microgrid_analysis/__init__.py +0 -0
  62. skypro/common/microgrid_analysis/bill_match.py +136 -0
  63. skypro/common/microgrid_analysis/breakdown.py +185 -0
  64. skypro/common/microgrid_analysis/daily_gains.py +41 -0
  65. skypro/common/microgrid_analysis/output.py +371 -0
  66. skypro/common/notice/__init__.py +0 -0
  67. skypro/common/notice/notice.py +24 -0
  68. skypro/common/rate_utils/__init__.py +0 -0
  69. skypro/common/rate_utils/friendly_summary.py +60 -0
  70. skypro/common/rate_utils/osam.py +150 -0
  71. skypro/common/rate_utils/to_dfs.py +134 -0
  72. skypro/common/rates/README.md +41 -0
  73. skypro/common/rates/__init__.py +0 -0
  74. skypro/common/rates/rates.py +308 -0
  75. skypro/common/rates/supply_point.py +18 -0
  76. skypro/common/rates/time_varying_value.py +61 -0
  77. skypro/common/timeutils/__init__.py +1 -0
  78. skypro/common/timeutils/clock_time_period.py +56 -0
  79. skypro/common/timeutils/dayed_period.py +33 -0
  80. skypro/common/timeutils/days.py +38 -0
  81. skypro/common/timeutils/math.py +21 -0
  82. skypro/common/timeutils/math_wallclock.py +35 -0
  83. skypro/common/timeutils/month_str.py +41 -0
  84. skypro/common/timeutils/settlement_periods.py +14 -0
  85. skypro/common/timeutils/timeseries.py +26 -0
  86. skypro/main.py +134 -0
  87. skypro/reporting_webapp/README.md +21 -0
  88. skypro/reporting_webapp/__init__.py +0 -0
  89. skypro/reporting_webapp/example_config.yaml +5 -0
  90. skypro/reporting_webapp/main.py +357 -0
  91. skypro-1.2.0.dist-info/METADATA +109 -0
  92. skypro-1.2.0.dist-info/RECORD +95 -0
  93. skypro-1.2.0.dist-info/WHEEL +4 -0
  94. skypro-1.2.0.dist-info/entry_points.txt +5 -0
  95. 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