Skip to content

Commit

Permalink
Support for Jenks natural breaks optimisation.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Hale committed Feb 1, 2024
1 parent f68170f commit 57d83ad
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 20 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ jobs:
- name: Run tests
run: |
export PYTHONPATH=src/modules
python -m pip install --upgrade pip
python -m pip install jenkspy
python -m unittest discover tests
- name: Build zip
run: |
wget -O jenkspy.zip https://github.com/pulquero/jenkspy/archive/refs/heads/master.zip
unzip jenkspy.zip
mkdir dist
mkdir dist/pyscript
mkdir dist/pyscript_packages
cp -R src/* dist/pyscript/
cp -R jenkspy-master/jenkspy dist/pyscript_packages/
cd dist
zip -r agile-powerwall.zip * -x "*/__pycache__/*" "*/__pycache__/"
- name: Release zips
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ using
`tariff_breaks`: Powerwall currently only supports four pricing levels: Peak, Mid-Peak, Off-Peak and Super Off-Peak.
Dynamic pricing therefore has to be mapped to these four levels.
The `tariff_breaks` represent the thresholds for each level.
So, by default, anything below £0.10 is mapped to Super Off-Peak, between £0.10 and £0.20 to Off-Peak, between £0.20 and £0.30 to Mid-peak, and above £0.30 to Peak.
So, by default, anything below £0.10 is mapped to Super Off-Peak, between £0.10 and £0.20 to Off-Peak, between £0.20 and £0.30 to Mid-peak, and above £0.30 to Peak. **Use** `tariff_breaks: jenks` **to optimally calculate the breaks.**

`plunge_pricing_tariff_breaks`: similar to above, but applied if there are any plunge (negative) prices.

Expand Down
48 changes: 29 additions & 19 deletions src/modules/powerwall_tariff.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import datetime as dt
import itertools
import sys

if "/config/pyscript_packages" not in sys.path:
sys.path.append("/config/pyscript_packages")
import jenkspy


EXCLUSIVE_OFFSET = 0.000001
Expand All @@ -9,6 +14,7 @@

CHARGE_NAMES = ["SUPER_OFF_PEAK", "OFF_PEAK", "PARTIAL_PEAK", "ON_PEAK"]

PRICE_KEY = "value_inc_vat"
PRICE_CAP = 1.00


Expand Down Expand Up @@ -74,9 +80,9 @@ def add(self, import_rate, export_rate):
self._periods.append((self._start, self._end))
self._start = import_rate["start"]
self._end = import_rate["end"]
self.import_pricing.add(import_rate["value_inc_vat"])
self.import_pricing.add(import_rate[PRICE_KEY])
if export_rate is not None:
self.export_pricing.add(export_rate["value_inc_vat"])
self.export_pricing.add(export_rate[PRICE_KEY])

def get_periods(self):
if self._start is not None:
Expand All @@ -97,15 +103,15 @@ def get_export_value(self):


def lowest_rates(rates, hrs):
prices = [r["value_inc_vat"] for r in rates]
prices = [r[PRICE_KEY] for r in rates]
prices.sort()
n = round(2.0*float(hrs))
limit = prices[n-1] if n <= len(prices) else prices[-1]
return limit + EXCLUSIVE_OFFSET


def highest_rates(rates, hrs):
prices = [r["value_inc_vat"] for r in rates]
prices = [r[PRICE_KEY] for r in rates]
prices.sort(reverse=True)
n = round(2.0*float(hrs))
limit = prices[n-1] if n <= len(prices) else prices[-1]
Expand Down Expand Up @@ -235,29 +241,33 @@ def get_schedules(config, day_date, import_rates, export_rates):

plunge_pricing = False
for rate in day_import_rates:
if rate["value_inc_vat"] < 0.0:
if rate[PRICE_KEY] < 0.0:
plunge_pricing = True
break

if "plunge_pricing_tariff_breaks" in config and plunge_pricing:
configured_breaks = config["plunge_pricing_tariff_breaks"]
else:
configured_breaks = config["tariff_breaks"]
if len(configured_breaks) != len(CHARGE_NAMES)-1:
if type(configured_breaks) == list and len(configured_breaks) != len(CHARGE_NAMES)-1:
raise ValueError(f"{len(CHARGE_NAMES)-1} breaks must be specified")

breaks = []
for br in configured_breaks:
if isinstance(br, float) or isinstance(br, int):
v = br
elif isinstance(br, str) and '(' in br and br[-1] == ')':
sep = br.index('(')
func_name = br[:sep]
func_args = br[sep+1:-1].split(',')
v = RATE_FUNCS[func_name](day_import_rates, *func_args)
else:
raise ValueError(f"Invalid threshold: {br}")
breaks.append(v)
if configured_breaks == "jenks":
bounds = jenkspy.jenks_breaks([r[PRICE_KEY] for r in day_import_rates], n_classes=len(CHARGE_NAMES))
breaks = bounds[1:-1]
else:
breaks = []
for br in configured_breaks:
if isinstance(br, float) or isinstance(br, int):
v = br
elif isinstance(br, str) and '(' in br and br[-1] == ')':
sep = br.index('(')
func_name = br[:sep]
func_args = br[sep+1:-1].split(',')
v = RATE_FUNCS[func_name](day_import_rates, *func_args)
else:
raise ValueError(f"Invalid threshold: {br}")
breaks.append(v)

configured_import_pricing = config.get("import_tariff_pricing")
if configured_import_pricing is None:
Expand All @@ -276,7 +286,7 @@ def get_schedules(config, day_date, import_rates, export_rates):
schedules.append(Schedule(charge_name, import_pricing, export_pricing))

for import_rate, export_rate in itertools.zip_longest(day_import_rates, day_export_rates):
import_cost = import_rate['value_inc_vat']
import_cost = import_rate[PRICE_KEY]
schedule = None
for i, br in enumerate(breaks):
if import_cost < br:
Expand Down

0 comments on commit 57d83ad

Please sign in to comment.