Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/modules/Pricing_PVPCes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# PVPCes Pricing Module

## Introduction

The PVPCes (Precio Voluntario para el Pequeño Consumidor) module fetches regulated electricity prices for Spain's small consumer tariff from the REE (Red Eléctrica de España) API at `api.esios.ree.es`. Prices are published daily at 20:30 CET for the following day and vary significantly by hour (typically 300–400% spread between cheapest and most expensive hours).

An API token is required; register at [https://www.esios.ree.es/en/](https://www.esios.ree.es/en/).

### Status

| Detail | Value |
| --------------- | --------------------------------------- |
| **Module Name** | PVPCesPricing |
| **Module Type** | Pricing |
| **Features** | Import price, cheapest-window selection |
| **Status** | Implemented, Stable |

## Configuration

```json
"pricing": {
"PVPCes": {
"enabled": true,
"token": "your-esios-api-token-here"
}
}
```

### Parameters

| Parameter | Value |
| --------- | ----- |
| `enabled` | *required* `true` or `false` |
| `token` | *required* API token from esios.ree.es |

## Cheapest-Window Scheduling (Flex Cheaper)

The PVPCes module exposes a `getCheapestStartHour(numHours, ini, end)` method that finds the cheapest contiguous block of hours within a configured window. This is designed for overnight charging where you need, for example, 5 hours of charging within a 22:00–07:00 window and want to pick the cheapest consecutive slot.

| Parameter | Description |
| ---------- | ----------- |
| `numHours` | Number of consecutive hours needed |
| `ini` | Start of the allowed window (hour 0–23) |
| `end` | End of the allowed window (hour 0–23); if less than `ini`, the window spans midnight |

## Cache Behaviour

Prices are fetched once per day. The cache expires at midnight (detected by comparing the current hour with the hour of the last fetch). A failed network request does not update the cache timestamp, allowing a retry on the next poll cycle.

## Policy Integration

See [Pricing_Static.md](Pricing_Static.md) for policy rule examples using `getImportPrice()` and `getExportPrice()`.

## Dashboard

When any Pricing module is active, the current import and export prices are displayed on the main dashboard and refreshed every 30 seconds.
81 changes: 81 additions & 0 deletions docs/modules/Pricing_Static.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Static Pricing Module

## Introduction

The Static Pricing module provides fixed import and export electricity prices to TWCManager for environments where dynamic pricing APIs are not available or not needed. Prices are configured directly in `config.json`.

### Status

| Detail | Value |
| --------------- | ------------------------- |
| **Module Name** | StaticPricing |
| **Module Type** | Pricing |
| **Features** | Import price, Export price |
| **Status** | Implemented, Stable |

## Configuration

Add a `pricing` section to your `config.json`:

```json
"pricing": {
"Static": {
"enabled": true,
"peak": {
"import": 0.25,
"export": 0.10
}
}
}
```

### Parameters

| Parameter | Value |
| ----------------- | ----- |
| `enabled` | *required* `true` or `false` |
| `peak.import` | *required* Import price per kWh (numeric) |
| `peak.export` | *required* Export price per kWh (numeric) |

## Policy Integration

Once configured, pricing data is available to the policy engine via the `getImportPrice()` and `getExportPrice()` functions. Example policy rules are provided in the `policy.extend.emergency` section of `config.json`.

### Stop charging when import price exceeds a maximum

```json
"config": {
"maxImportPrice": 0.30
},
"policy": {
"extend": {
"emergency": [
{ "name": "Import Price Too High",
"match": [ "getImportPrice()" ],
"condition": [ "gt" ],
"value": [ "config.maxImportPrice" ],
"charge_amps": 0 }
]
}
}
```

### Stop charging when export price exceeds import price

```json
"policy": {
"extend": {
"emergency": [
{ "name": "Export Price Favourable",
"match": [ "getExportPrice()" ],
"condition": [ "gt" ],
"value": [ "getImportPrice()" ],
"charge_amps": 0 }
]
}
}
```

## Dashboard

When any Pricing module is active, the current import and export prices are displayed on the main dashboard and refreshed every 30 seconds.
64 changes: 64 additions & 0 deletions docs/modules/Pricing_aWATTar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# aWATTar Pricing Module

## Introduction

The aWATTar Pricing module fetches real-time day-ahead electricity spot market prices from the [aWATTar API](https://www.awattar.at/) for Austria and Germany. Prices reflect the hourly EPEX spot market and can be used to stop charging during expensive periods.

aWATTar requests that clients limit API queries to once every 15 minutes. This module enforces that limit automatically.

### Status

| Detail | Value |
| --------------- | ------------------------- |
| **Module Name** | aWATTarPricing |
| **Module Type** | Pricing |
| **Features** | Import price (spot market) |
| **Status** | Implemented, Stable |

### Note

aWATTar provides import (buy) prices only. Export prices will always return 0 from this module. If you need export pricing, combine with the Static Pricing module using `multiPrice: "add"`.

## Configuration

```json
"pricing": {
"aWATTar": {
"enabled": true
}
}
```

### Parameters

| Parameter | Value |
| --------- | ----- |
| `enabled` | *required* `true` or `false` |

### Multiple Pricing Modules

If you run both aWATTar and Static Pricing simultaneously, configure the `multiPrice` policy in the `pricing.policy` section:

```json
"pricing": {
"policy": {
"multiPrice": "first"
},
"aWATTar": {
"enabled": true
}
}
```

| Policy | Behaviour |
| ------- | --------- |
| `first` | Use the first non-zero price found (default) |
| `add` | Sum prices from all active modules |

## Policy Integration

See [Pricing_Static.md](Pricing_Static.md) for policy rule examples using `getImportPrice()` and `getExportPrice()`.

## Dashboard

When any Pricing module is active, the current import and export prices are displayed on the main dashboard and refreshed every 30 seconds.
52 changes: 52 additions & 0 deletions etc/twcmanager/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@
#"scheduledLimit": 50,
#"nonScheduledLimit": -1,

# Maximum import price (per kWh) above which charging will be stopped.
# Used with the emergency policy examples in the policy section below.
# Requires a Pricing module to be configured.
#"maxImportPrice": 0.30,

# Deprecated, use logLevel instead
# Choose how much debugging info to output.
# 0 is no output other than errors.
Expand Down Expand Up @@ -327,6 +332,21 @@
#
# They should primarily be used to abort charging when necessary.
"emergency":[
# Stop charging if import price exceeds the configured maximum.
# Requires a Pricing module and "maxImportPrice" set in config above.
#{ "name": "Import Price Too High",
# "match": [ "getImportPrice()" ],
# "condition": [ "gt" ],
# "value": [ "config.maxImportPrice" ],
# "charge_amps": 0 },

# Stop charging if export price exceeds import price (sell back to grid instead).
# Requires a Pricing module that provides both import and export prices.
#{ "name": "Export Price Favourable",
# "match": [ "getExportPrice()" ],
# "condition": [ "gt" ],
# "value": [ "getImportPrice()" ],
# "charge_amps": 0 },
],
# Rules in the before section here are evaluated after the Charge Now rule
"before":[
Expand Down Expand Up @@ -794,5 +814,37 @@
"mqtt_pass": "teslamate",
"mqtt_prefix": "teslamate"
}
},
"pricing": {
# Policy for combining prices when multiple pricing modules are active.
# "add" - sum prices from all active modules
# "first" - use the first non-zero price found
"policy": {
"multiPrice": "first"
},

# Static Pricing: configure fixed import/export prices when no dynamic
# pricing API is available.
#"Static": {
# "enabled": false,
# "peak": {
# "import": 0.25,
# "export": 0.10
# }
#},

# aWATTar: Austrian/German day-ahead spot market pricing.
# Queries https://api.awattar.at/v1/marketdata (rate-limited to once per 15 min).
#"aWATTar": {
# "enabled": false
#},

# PVPCes: Spanish PVPC regulated small-consumer tariff.
# Prices are published daily at 20:30 CET by REE (api.esios.ree.es).
# Requires an API token from https://www.esios.ree.es/en/
#"PVPCes": {
# "enabled": false,
# "token": "your-esios-api-token-here"
#}
}
}
16 changes: 16 additions & 0 deletions lib/TWCManager/Control/HTTPControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,23 @@ def do_API_GET(self):
json_data = json.dumps(master.getModuleByName("Policy").charge_policy)
self.wfile.write(json_data.encode("utf-8"))

elif self.url.path == "/api/getPricing":
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()

json_data = json.dumps(
{
"export": master.getExportPrice(),
"import": master.getImportPrice(),
}
)
self.wfile.write(json_data.encode("utf-8"))

elif self.url.path == "/api/getSlaveTWCs":
data = {}
totals = {
"carsCharging": 0,
"lastAmpsOffered": 0,
"lifetimekWh": 0,
"maxAmps": 0,
Expand Down Expand Up @@ -313,12 +327,14 @@ def do_API_GET(self):
data[TWCID]["lastAtHome"] = vehicle.atHome
data[TWCID]["lastTimeToFullCharge"] = vehicle.timeToFullCharge

totals["carsCharging"] += slaveTWC.isCharging
totals["lastAmpsOffered"] += slaveTWC.lastAmpsOffered
totals["lifetimekWh"] += slaveTWC.lifetimekWh
totals["maxAmps"] += slaveTWC.maxAmps
totals["reportedAmpsActual"] += slaveTWC.reportedAmpsActual

data["total"] = {
"carsCharging": totals["carsCharging"],
"lastAmpsOffered": round(totals["lastAmpsOffered"], 2),
"lifetimekWh": totals["lifetimekWh"],
"maxAmps": totals["maxAmps"],
Expand Down
19 changes: 19 additions & 0 deletions lib/TWCManager/Control/themes/Default/jsrefresh.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ $(document).ready(function() {
requestSlaves();
});

// AJAJ refresh for getPricing API call (poll every 30s - pricing updates infrequently)
$(document).ready(function() {
function requestPricing() {
$.ajax({
url: "/api/getPricing",
dataType: "text",
cache: false,
success: function(data) {
var json = $.parseJSON(data);
$('#importPrice').html(json['import']);
$('#exportPrice').html(json['export']);
}
});
setTimeout(requestPricing, 30000);
}

requestPricing();
});

$(document).ready(function() {
$("#start_chargenow").click(function(e) {
e.preventDefault();
Expand Down
11 changes: 10 additions & 1 deletion lib/TWCManager/Control/themes/Default/showStatus.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@
<th>Current Charger Load</th>
<td><div id="chargerLoadWatts"></div></td><td>watts</td>
</tr>

{% if master.getModulesByType("Pricing") %}
<tr>
<th>Import Price</th>
<td><div id='importPrice'></div></td><td>per kWh</td>
</tr>
<tr>
<th>Export Price</th>
<td><div id='exportPrice'></div></td><td>per kWh</td>
</tr>
{% endif %}
<tr>
<th>Number of Cars Charging</th>
<td>
Expand Down
8 changes: 8 additions & 0 deletions lib/TWCManager/Policy/Policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ def enforcePolicy(self, policy, updateLatch=False):
limit = -1
self.master.queue_background_task({"cmd": "applyChargeLimit", "limit": limit})

# If at least one pricing module is active, fetch current pricing
if len(self.master.getModulesByType("Pricing")) > 0:
self.master.queue_background_task({"cmd": "getPricing"})

# Report current policy via Status modules
for module in self.master.getModulesByType("Status"):
module["ref"].setStatus(
Expand Down Expand Up @@ -308,6 +312,10 @@ def policyValue(self, value):
return self.master.getMaxAmpsForTargetGridUsage()
elif value == "checkScheduledCharging()":
return self.master.checkScheduledCharging()
elif value == "getImportPrice()":
return self.master.getImportPrice()
elif value == "getExportPrice()":
return self.master.getExportPrice()

# If value is tiered, split it up
if value.find(".") != -1:
Expand Down
Loading
Loading