Skip to content

Commit

Permalink
Support for status messages via input_text helper.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Hale committed Mar 23, 2024
1 parent 4e6fe13 commit 83925a3
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 89 deletions.
161 changes: 82 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,82 @@
# Agile Powerwall
Home Assistant Pyscript-based integration that uploads dynamic pricing to Tesla Powerwalls.

This is primarily designed to sync Octopus Agile prices to Tesla Powerwalls.
It glues together two pieces of software

* [Home Assistant Octopus Energy](https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy)
* [TeslaPy](https://github.com/tdorssers/TeslaPy)

using

* [Pyscript](https://github.com/custom-components/pyscript).


## Installation

1. Install Home Assistant Octopus Energy integration.
2. Install Pyscript integration.
3. Unzip the [release zip](https://github.com/pulquero/agile-powerwall/releases/latest) into the Home Assistant directory `/config`.
4. Add Pyscript app configuration:

pyscript:
apps:
powerwall:
email: <username/email>
refresh_token: <refresh_token>
tariff_name: Agile
tariff_provider: Octopus
import_mpan: <mpan>
tariff_breaks: [0.10, 0.20, 0.30]
import_tariff_pricing: ["average", "average", "maximum", "maximum"]
plunge_pricing_tariff_breaks: [0.0, 0.10, 0.30]

## Configuration

`email`: E-mail address of your Tesla account.

`refresh_token`: One-off refresh token (see e.g. <https://github.com/DoctorMcKay/chromium-tesla-token-generator>)

`tariff_name`: name of the tariff.

`tariff_provider`: name of the tariff provider.

`import_mpan`: MPAN to use for import rates

`export_mpan`: MPAN to use for export rates if you have one

`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. **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.

`import_tariff_pricing`: determines how to calculate the price of each import pricing level from the actual prices assigned to a level.

`export_tariff_pricing`: determines how to calculate the price of each export pricing level from the actual prices assigned to a level.


### Computed thresholds

As well as numeric thresholds, the following computed thresholds are also supported:

`lowest(num_hours)`: sets the threshold at the price to include the cheapest `num_hours` hours.

`highest(num_hours)`: sets the threshold at the price to exclude the most expensive `num_hours` hours.

e.g.:

tariff_breaks: ["lowest(2)", 0.20, 0.30]


### Pricing formulas

`average`: the average of all the prices. If the average is negative, it is set to zero.

`minimum`: the minimum of all the prices. If the minimum is negative, it is set to zero.

`maximum`: the maximum of all the prices.
# Agile Powerwall
Home Assistant Pyscript-based integration that uploads dynamic pricing to Tesla Powerwalls.

This is primarily designed to sync Octopus Agile prices to Tesla Powerwalls.
It glues together two pieces of software

* [Home Assistant Octopus Energy](https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy)
* [TeslaPy](https://github.com/tdorssers/TeslaPy)

using

* [Pyscript](https://github.com/custom-components/pyscript).


## Installation

1. Install Home Assistant Octopus Energy integration.
2. Install Pyscript integration.
3. Unzip the [release zip](https://github.com/pulquero/agile-powerwall/releases/latest) into the Home Assistant directory `/config`.
4. Add Pyscript app configuration:

pyscript:
apps:
powerwall:
email: <username/email>
refresh_token: <refresh_token>
tariff_name: Agile
tariff_provider: Octopus
import_mpan: <mpan>
tariff_breaks: [0.10, 0.20, 0.30]
import_tariff_pricing: ["average", "average", "maximum", "maximum"]
plunge_pricing_tariff_breaks: [0.0, 0.10, 0.30]

5. Optionally, create an `input_text` helper called `powerwall_tariff_update_status` if you want status messages.


## Configuration

`email`: E-mail address of your Tesla account.

`refresh_token`: One-off refresh token (see e.g. <https://github.com/DoctorMcKay/chromium-tesla-token-generator>)

`tariff_name`: name of the tariff.

`tariff_provider`: name of the tariff provider.

`import_mpan`: MPAN to use for import rates

`export_mpan`: MPAN to use for export rates if you have one

`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. **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.

`import_tariff_pricing`: determines how to calculate the price of each import pricing level from the actual prices assigned to a level.

`export_tariff_pricing`: determines how to calculate the price of each export pricing level from the actual prices assigned to a level.


### Computed thresholds

As well as numeric thresholds, the following computed thresholds are also supported:

`lowest(num_hours)`: sets the threshold at the price to include the cheapest `num_hours` hours.

`highest(num_hours)`: sets the threshold at the price to exclude the most expensive `num_hours` hours.

e.g.:

tariff_breaks: ["lowest(2)", 0.20, 0.30]


### Pricing formulas

`average`: the average of all the prices. If the average is negative, it is set to zero.

`minimum`: the minimum of all the prices. If the minimum is negative, it is set to zero.

`maximum`: the maximum of all the prices.
17 changes: 16 additions & 1 deletion src/apps/powerwall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,27 @@ def refresh_next_day_rates(mpan, rates, **kwargs):
update_powerwall_tariff()


def set_status_message(value):
try:
input_text.powerwall_tariff_update_status = value
except:
pass


def update_powerwall_tariff():
try:
IMPORT_RATES.is_valid()
except ValueError as err:
debug(str(err))
set_status_message("Waiting for updated import tariffs")
return

if EXPORT_MPAN:
try:
EXPORT_RATES.is_valid()
except ValueError as err:
debug(str(err))
set_status_message("Waiting for updated export tariffs")
return

_update_powerwall_tariff()
Expand All @@ -80,7 +89,11 @@ def update_powerwall_tariff():


def _update_powerwall_tariff():
tariff_data = tariff.calculate_tariff_data(pyscript.app_config, dt.date.today(), IMPORT_RATES, EXPORT_RATES)
schedules = tariff.get_schedules(pyscript.app_config, dt.date.today(), IMPORT_RATES, EXPORT_RATES)
if schedules is None:
return

tariff_data = tariff.to_tariff_data(pyscript.app_config, schedules)

debug(f"Tariff data:\n{tariff_data}")

Expand All @@ -91,6 +104,8 @@ def _update_powerwall_tariff():
)

debug("Powerwall updated")
breaks = [s.upper_bound for s in schedules if s.upper_bound is not None]
set_status_message(f"Tariff data updated at {dt.datetime.now()} (breaks: {breaks})")


@service("powerwall.refresh_tariff_data")
Expand Down
28 changes: 19 additions & 9 deletions src/modules/powerwall_tariff.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ def is_valid(self):
raise ValueError(f"Current to next day rates are not contiguous: {current_day_end} {next_day_start}")

def between(self, start, end):
all_rates = itertools.chain(self.previous_day, self.current_day, self.next_day)
return [rate for rate in all_rates if rate["start"] >= start and rate["end"] <= end]
if self.previous_day or self.current_day or self.next_day: # pyscript doesn't like empty list comprehensions
all_rates = itertools.chain(self.previous_day, self.current_day, self.next_day)
return [rate for rate in all_rates if rate["start"] >= start and rate["end"] <= end]
else:
return []

def reset(self):
self._previous_day_updated = False
Expand All @@ -67,8 +70,9 @@ def reset(self):


class Schedule:
def __init__(self, charge_name, import_pricing, export_pricing):
def __init__(self, charge_name, upper_bound, import_pricing, export_pricing):
self.charge_name = charge_name
self.upper_bound = upper_bound
self.import_pricing = import_pricing
self.export_pricing = export_pricing
self._periods = []
Expand Down Expand Up @@ -294,9 +298,10 @@ def get_schedules(config, day_date, import_rates, export_rates):

schedules = []
for i, charge_name in enumerate(CHARGE_NAMES):
upper_bound = breaks[i] if i < len(breaks) else None
import_pricing = create_pricing(configured_import_pricing[i])
export_pricing = create_pricing(configured_export_pricing[i])
schedules.append(Schedule(charge_name, import_pricing, export_pricing))
schedules.append(Schedule(charge_name, upper_bound, import_pricing, export_pricing))

for import_rate, export_rate in itertools.zip_longest(day_import_rates, day_export_rates):
import_cost = import_rate[PRICE_KEY]
Expand Down Expand Up @@ -325,11 +330,7 @@ def to_charge_period_json(start_day_of_week, end_day_of_week, period):
}


def calculate_tariff_data(config, day_date, import_rates, export_rates):
schedules = get_schedules(config, day_date, import_rates, export_rates)
if schedules is None:
return

def to_tariff_data(config, schedules):
tou_periods = {}
buy_price_info = {}
sell_price_info = {}
Expand Down Expand Up @@ -367,3 +368,12 @@ def calculate_tariff_data(config, day_date, import_rates, export_rates):
"Winter": {}}}
}
return tariff_data


def calculate_tariff_data(config, day_date, import_rates, export_rates):
schedules = get_schedules(config, day_date, import_rates, export_rates)
if schedules is None:
return

tariff_data = to_tariff_data(config, schedules)
return tariff_data

0 comments on commit 83925a3

Please sign in to comment.