Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for pricing modules for tracking pricing #210

Draft
wants to merge 2 commits into
base: v1.2.1
Choose a base branch
from
Draft
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
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,21 @@ This can be useful if you would like to track charger activity and use it for gr
| [MySQL](docs/modules/Logging_MySQL.md) | Log data to a MySQL Database |
| [SQLite](docs/modules/Logging_SQLite.md) | Log data to a SQLite Database |

### Pricing Interfaces

| Platform | Details |
| --------------------------- | ----------------------- |
| [aWATTar](docs/modules/aWATTar_Pricing.md) | aWATTar dynamic pricing API |
| [Static](docs/modules/Static_Pricing.md) | Configure static pricing detail |

### Status Interfaces

Status interfaces publish TWCManager status information to external systems. Current Status interfaces are:

| Platform | Status | Details |
| ---------------- | ---------------- | ----------------------- |
| [HomeAssistant](docs/modules/Status_HASS.md) | Available v1.0.1 | Provides HASS sensors to monitor TWCManager State |
| [MQTT](docs/modules/Status_MQTT.md) | Available v1.0.1 | Publishes MQTT topics to monitor TWCManager State |
| Platform | Details |
| ---------------- | ----------------------- |
| [HomeAssistant](docs/modules/Status_HASS.md) | Provides HASS sensors to monitor TWCManager State |
| [MQTT](docs/modules/Status_MQTT.md) | Publishes MQTT topics to monitor TWCManager State |

### Vehicle Interfaces

Expand Down
5 changes: 5 additions & 0 deletions TWCManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
"EMS.TED",
"Status.HASSStatus",
"Status.MQTTStatus",
"Pricing.aWATTarPricing",
"Pricing.PVPCesPricing",
"Pricing.StaticPricing",
]

# Enable support for Python Visual Studio Debugger
Expand Down Expand Up @@ -223,6 +226,8 @@ def background_tasks_thread(master):
check_green_energy()
elif task["cmd"] == "getLifetimekWh":
master.getSlaveLifetimekWh()
elif task["cmd"] == "getPricing":
master.getPricing()
elif task["cmd"] == "getVehicleVIN":
master.getVehicleVIN(task["slaveTWC"], task["vinPart"])
elif task["cmd"] == "snapHistoryData":
Expand Down
39 changes: 39 additions & 0 deletions docs/modules/Pricing_Static.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Static Power Pricing Module

## Introduction

The Static Power Pricing Module allows configuration of power prices in environments where you do not have access to an API or dynamic source of pricing data.

It currently only allows configuration of peak import and export rates.

### Status

| Detail | Value |
| --------------- | ------------------------------ |
| **Module Name** | Static |
| **Module Type** | Power Pricing Module (Pricing) |
| **Features** | Export, Import Pricing |
| **Status** | New, Work In Progress |

## Configuration

The following table shows the available configuration parameters for the Static Pricing module.

| Parameter | Value |
| ------------- | ------------- |
| enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will use this pricing module. |
| peak > export | *optional* The export (sell to grid) price of power during the peak period |
| peak > import | *optional* The import (buy from grid) price of power during the peak period |

### JSON Configuration Example

```
"pricing": {
"Static": {
"enabled": true,
"peak": {
"import": 0.10,
"export": 0.01
}
}
```
35 changes: 35 additions & 0 deletions docs/modules/Pricing_aWATTar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# aWATTar Power Pricing Module

## Introduction

This module queries the Market Price data published by aWATTar for their energy customers. Please note that this is market pricing and not retail pricing, so whilst it provides insight into the price movements, it does not represent the exact cost to user.

Currently, we only read the first price returned in the API call, which is the current market price.

aWATTar's policy is to allow up to 100 queries per day of the data service. This equates to around 4 queries per hour or one every 15 minutes. We limit the query interval to this amount.

### Status

| Detail | Value |
| --------------- | ------------------------------ |
| **Module Name** | aWATTarPricing |
| **Module Type** | Power Pricing Module (Pricing) |
| **Features** | Import Pricing |
| **Status** | New |

## Configuration

The following table shows the available configuration parameters for the aWATTer pricing module.

| Parameter | Value |
| ----------- | ------------- |
| enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the aWATTar API for pricing. |

### JSON Configuration Example

```
"pricing": {
"aWATTar": {
"enabled": true,
}
```
37 changes: 37 additions & 0 deletions etc/twcmanager/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,43 @@
"path": "/etc/twcmanager/twcmanager.sqlite"
}
},
"pricing":{
"policy": {
# The multiPrice policy determines what we do when we have multiple
# pricing modules enabled. This might happen if your import and export
# pricing is tracked by different modules (for example, static export)
# and dynamic import).
# The default is to add all prices together.
# Other options are:
# - First: Takes the first non-zero value from the queried modules
"multiPrice": "add"
},
"aWATTar": {
# Enable this module if you are a customer of aWATTar in Austria
"enabled": false
},
"PVPCes": {
# Enable this module if you are a customer under PVPC in Spain
# You need to get a personal token from https://api.esios.ree.es/
"enabled": false,
"token": "xxx"
},
"Static": {
# The static pricing module allows you to specify static pricing, if
# your utility or provider do not provide a dynamic API. This still
# allows leveraging of pricing-related policy, even without an API.

"enabled": false,

# At this point, we only provide flat peak pricing, however future
# revisions should provide better tunability
# Pricing should be in units (dollar, euro, etc) per-kWh notation
"peak": {
"import": 0.20,
"export": 0.09
}
}
},
"sources":{
# This section is where we configure the various sources that we retrieve our generation and consumption
# values for our solar system from.
Expand Down
11 changes: 11 additions & 0 deletions lib/TWCManager/Control/HTTPControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ def do_API_GET(self):
)
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 = {
Expand Down
4 changes: 4 additions & 0 deletions lib/TWCManager/Policy/Policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,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"})

def fireWebhook(self, hook):
policy = self.getPolicyByName(self.active_policy)
if policy:
Expand Down
192 changes: 192 additions & 0 deletions lib/TWCManager/Pricing/PVPCesPricing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
from datetime import datetime
from datetime import timedelta

class PVPCesPricing:

import requests
import time

# https://www.esios.ree.es/es/pvpc publishes at 20:30CET eveyday the prices for next day
# There is no limitation to fetch prices as it's updated onces a day
cacheTime = 1
capabilities = {
"AdvancePricing": True
}
config = None
configConfig = None
configPvpc = None
exportPrice = 0
fetchFailed = False
importPrice = 0
lastFetch = 0
status = False
timeout = 10
headers = {}
todayImportPrice = {}

def __init__(self, master):

self.master = master
self.config = master.config
try:
self.configConfig = master.config["config"]
except KeyError:
self.configConfig = {}

try:
self.configPvpc = master.config["pricing"]["PVPCes"]
except KeyError:
self.configPvpc = {}

self.status = self.configPvpc.get("enabled", self.status)
self.debugLevel = self.configConfig.get("debugLevel", 0)

token=self.configPvpc.get("token")
if self.status:
self.headers = {
'Accept': 'application/json; application/vnd.esios-api-v1+json',
'Content-Type': 'application/json',
'Host': 'api.esios.ree.es',
'Cookie': '',
}
self.headers['Authorization']="Token token="+token

# Unload if this module is disabled or misconfigured
if not self.status:
self.master.releaseModule("lib.TWCManager.Pricing", self.__class__.__name__)
return None

def getCapabilities(self, capability):
# Allows query of module capabilities
return self.capabilities.get(capability, False)

def getExportPrice(self):

if not self.status:
self.master.debugLog(
10,
"$PVPCes",
"PVPCes Pricing Module Disabled. Skipping getExportPrice",
)
return 0

# Perform updates if necessary
self.update()

# Return current export price
return float(self.exportPrice)

def getImportPrice(self):

if not self.status:
self.master.debugLog(
10,
"$PVPCes",
"PVPCes Pricing Module Disabled. Skipping getImportPrice",
)
return 0

# Perform updates if necessary
self.update()



# Return current import price
return float(self.importPrice)

def update(self):

# Fetch the current pricing data from the https://www.esios.ree.es/es/pvpc API
self.fetchFailed = False
now=datetime.now()
tomorrow=datetime.now() + timedelta(days=1)
if self.lastFetch == 0 or (now.hour < self.lastFetch.hour):
# Cache not feched or was feched yesterday. Fetch values from API.
ini=str(now.year)+"-"+str(now.month)+"-"+str(now.day)+"T"+"00:00:00"
end=str(tomorrow.year)+"-"+str(tomorrow.month)+"-"+str(tomorrow.day)+"T"+"23:00:00"

url = "https://api.esios.ree.es/indicators/1014?start_date="+ini+"&end_date="+end

try:
r = self.requests.get(url,headers=self.headers, timeout=self.timeout)
except self.requests.exceptions.ConnectionError as e:
self.master.debugLog(
4,
"$PVPCes",
"Error connecting to PVPCes API to fetch market pricing",
)
self.fetchFailed = True
return False

self.lastFetch= now

try:
r.raise_for_status()
except self.requests.exceptions.HTTPError as e:
self.master.debugLog(
4,
"$PVPCes",
"HTTP status "
+ str(e.response.status_code)
+ " connecting to PVPCes API to fetch market pricing",
)
return False

if r.json():
self.todayImportPrice=r.json()

if self.todayImportPrice:
try:
self.importPrice = float(
self.todayImportPrice['indicator']['values'][now.hour]['value']
)
# Convert MWh price to KWh
self.importPrice = round(self.importPrice / 1000,5)

except (KeyError, TypeError) as e:
self.master.debugLog(
4,
"$PVPCes",
"Exception during parsing PVPCes pricing",
)

def getCheapestStartHour(self,numHours,ini,end):
# Perform updates if necessary
self.update()

minPriceHstart=ini
if self.todayImportPrice:
try:
if end < ini:
# If the scheduled hours are bettween days we consider hours going from 0 to 47
# tomorrow 1am will be 25
end = 24 + end

i=ini
minPrice=999999999
while i<=(end-numHours):
j=0
priceH=0
while j<numHours:
price = float(self.todayImportPrice['indicator']['values'][i+j]['value'])

priceH = priceH + price

j=j+1
if priceH<minPrice:
minPrice=priceH
minPriceHstart=i
i=i+1


except (KeyError, TypeError) as e:
self.master.debugLog(
4,
"$PVPCes",
"Exception during cheaper pricing analice",
)

if minPriceHstart > 23:
minPriceHstart = minPriceHstart - 24

return minPriceHstart
Loading