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

New picing module for Spanish PVPC model #237

Open
wants to merge 20 commits into
base: pricing_modules
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dfa6109
Add files via upload
ngardiner Jan 21, 2021
7ac3cc2
Bump changelog and screenshot
ngardiner Jan 21, 2021
69903d5
Removed confusing statement from WebUI
ngardiner Jan 24, 2021
216754f
Merge branch 'v1.2.1' of https://github.com/ngardiner/TWCManager into…
ngardiner Jan 24, 2021
5a7c7f2
Provide the ability to override the stored API bearer/refresh tokens …
ngardiner Jan 30, 2021
d9170a3
Update Tesla API authentication to work with oAuth2 flow (no MFA supp…
ngardiner Feb 2, 2021
3ee4dfb
Remove spoofed UA headers from requests (not needed), standardise var…
ngardiner Feb 3, 2021
872cf7e
Black on TeslaAPI.py
MikeBishop Feb 3, 2021
e654722
New feature that allows to limit the max power TWC will take from the…
juanjoqg Feb 3, 2021
0825b6d
Avoid using a hardcode path for the PID file, take the path from conf…
juanjoqg Feb 4, 2021
4e781c0
Rollback to the namespace remove
juanjoqg Feb 4, 2021
5aec8ff
Bug fix in the limit amps from the grid integration with track green …
juanjoqg Feb 12, 2021
8666768
Change to ensure it just limit the amps from the grid when the right …
juanjoqg Feb 13, 2021
3514c10
New menu Graphs, it allows to represent energy graphs base on the SQL…
juanjoqg Feb 17, 2021
32f6d41
Change step on the graphs from seconds to minutes
juanjoqg Feb 18, 2021
6c7921d
New picing module for Spanish PVPC model, i adds a new schedule char…
juanjoqg Feb 20, 2021
8a4ee41
Merge remote-tracking branch 'origin/limit_amps_from_grid' into prici…
juanjoqg Feb 20, 2021
729a0bd
Merge remote-tracking branch 'origin/energy_graphs' into pricing_modules
juanjoqg Feb 20, 2021
4f78ad9
New feature related with the pricing modules that allows to schedule …
juanjoqg Feb 26, 2021
e676a37
New features related to the pricing modules, schedule charging relate…
juanjoqg Feb 26, 2021
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
1 change: 1 addition & 0 deletions TWCManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"Status.MQTTStatus",
"Pricing.aWATTarPricing",
"Pricing.StaticPricing",
"Pricing.PVPCesPricing",
]

# Enable support for Python Visual Studio Debugger
Expand Down
7 changes: 7 additions & 0 deletions etc/twcmanager/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,14 @@
"import": 0.20,
"export": 0.09
}
},
"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"
}

},
"sources":{
# This section is where we configure the various sources that we retrieve our generation and consumption
Expand Down
14 changes: 13 additions & 1 deletion lib/TWCManager/Control/HTTPControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,16 @@ def chargeScheduleDay(self, day):
page += "<td>" + self.checkBox("flex"+suffix,
today.get("flex", 0)) + "</td>"
page += "<td>Flex Charge</td>"
if master.getPricingInAdvanceAvailable():
page += "<td>" + self.checkBox("cheaper"+suffix,
today.get("cheaper", 0)) + "</td>"
page += "<td>Flex Cheaper</td>"
if not today.get("flex", 0):
page += "<td>" + self.optionList(self.hoursDurationList,
{"name": "actualH"+suffix,
"value": today.get("actualH", 1)}) + "</td>"
page += "<td>hours</td>"

page += "</tr>"
return page

Expand Down Expand Up @@ -709,11 +719,13 @@ def process_save_schedule(self):
master.settings["Schedule"][day] = {}
master.settings["Schedule"][day]["enabled"] = ""
master.settings["Schedule"][day]["flex"] = ""
master.settings["Schedule"][day]["cheaper"] = ""
master.settings["Schedule"][day]["actualH"] = ""

# Detect schedule keys. Rather than saving them in a flat
# structure, we'll store them multi-dimensionally
fieldsout = self.fields.copy()
ct = re.compile(r'(?P<trigger>enabled|end|flex|start)(?P<day>.*?)ChargeTime')
ct = re.compile(r'(?P<trigger>enabled|end|flex|cheaper|actualH|start)(?P<day>.*?)ChargeTime')
for key in self.fields:
match = ct.match(key)
if match:
Expand Down
189 changes: 189 additions & 0 deletions lib/TWCManager/Pricing/PVPCesPricing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
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
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 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

def getPricingInAdvanceAvailable(self):
return True

4 changes: 4 additions & 0 deletions lib/TWCManager/Pricing/StaticPricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,7 @@ def getImportPrice(self):
# Return current import price
return float(self.importPrice)

def getPricingInAdvanceAvailable(self):
# For future implementation
return False

2 changes: 2 additions & 0 deletions lib/TWCManager/Pricing/aWATTarPricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,6 @@ def update(self):
"Exception during parsing aWATTar pricing",
)

def getPricingInAdvanceAvailable(self):
return False

68 changes: 66 additions & 2 deletions lib/TWCManager/TWCMaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ def checkScheduledCharging(self):

# Check if we're within the hours we must use scheduledAmpsMax instead
# of nonScheduledAmpsMax
blnUseScheduledAmps = 0
ltNow = time.localtime()
blnUseScheduledAmps = 0
hourNow = ltNow.tm_hour + (ltNow.tm_min / 60)
timeSettings = self.getScheduledAmpsTimeFlex()
startHour = timeSettings[0]
Expand Down Expand Up @@ -145,6 +145,7 @@ def checkScheduledCharging(self):
and (daysBitmap & (1 << ltNow.tm_wday))
):
blnUseScheduledAmps = 1

return blnUseScheduledAmps

def convertAmpsToWatts(self, amps):
Expand Down Expand Up @@ -269,6 +270,26 @@ def getPricing(self):
self.exportPricingValues[module["name"]] = module["ref"].getExportPrice()
self.importPricingValues[module["name"]] = module["ref"].getImportPrice()

def getPricingInAdvanceAvailable(self):
for module in self.getModulesByType("Pricing"):
if module["ref"].getPricingInAdvanceAvailable():
return True
return False

def getCheaperDayChargeTime(self,dayName):
return self.settings["Schedule"][dayName]["cheaper"]

def getActualHDayChargeTime(self,dayName):
return self.settings["Schedule"][dayName]["actualH"]

def getCheapestStartHour(self,numHours,ini,end):
# We take the latest modul data
cheapestStartHour = ini
for module in self.getModulesByType("Pricing"):
cheapestStartHour = module["ref"].getCheapestStartHour(numHours,ini,end)

Copy link
Owner

@ngardiner ngardiner Feb 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few thoughts about this:

  • It's always possible that we could have more than one Pricing module running. The reason might be different data being provided by different modules. It doesn't matter too much but my thoughts are that the logic should be:
        for module in self.getModulesByType("Pricing"):
             moduleCheapestHour = module["ref"].getCheapestStartHour(numHours,ini,end)
             if moduleCheapestHour > cheapestStartHour:
                 cheapestStartHour = moduleCheapestHour
  • This will also require checking the ability for advance pricing per module. We could always just put a dummy function which returns ini if it doesn't support advanced pricing, but it's better to just check and not query if not imho, it saves having to add useless functions

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, just thinking out loud about the scheduled charging logic here:

  • I assume this is really only useful when not using green energy, though. Even the cheapest energy of the day might be more expensive when drawn from the grid rather than green energy.
  • I think there's somewhat of a hole in the scheduled charging logic, in that we basically look for the cheapest start hour to start a charge for a given duration, but it then may carry over a more expensive period after that, and we don't really protect against charging through a more expensive period overall, we just pick a cheap hour. I worry that people might think they are better off but end up worse off, eg it might trigger a charge to start at 1PM which is peak for renewables but then continue through to 7PM which is peak for grid pricing.
  • It would almost be better to say - how many hours during a 24 hour period do you want to charge your vehicle for, and then we choose the x cheapest hours over that day and start/stop per hour. The fault in that logic is that it assumes a vehicle is connected 24/7, and if it isn't, advanced pricing is not of much use to us as we have no dynamic data to respond to.

I'd be interested in feedback from those with power pricing APIs to hear how they'd want to use the functionality.

return cheapestStartHour

def getScheduledAmpsDaysBitmap(self):
return self.settings.get("scheduledAmpsDaysBitmap", 0x7F)

Expand All @@ -289,9 +310,49 @@ def getScheduledAmpsMax(self):
else:
return 0

def getActualHscheduledAmps(self):
return int(self.settings.get("actualHscheduledAmps", -1))

def getScheduledAmpsStartHour(self):
return int(self.settings.get("scheduledAmpsStartHour", -1))



def getScheduledAmpsCheaperFlex(self, startHour, endHour, daysBitmap):
# adjust the charge start time to minimize the cost
if not self.getPricingInAdvanceAvailable():
return (startHour, endHour, daysBitmap)

daysNames = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]
ltNow = time.localtime()
hourNow = ltNow.tm_hour + (ltNow.tm_min / 60)
dayName = daysNames[ltNow.tm_wday+1]

if self.getCheaperDayChargeTime(dayName):
self.debugLog(10,"TWCMaster","getCheaperDayChargeTime True")
if self.getScheduledAmpsFlexStart():
#If flex start is active the actual charge duration will be taken from the flex period established
if startHour < endHour:
numHours = endHour-startHour
else:
numHours = 24-startHour+endHour
else:
#If flex start is not active the actual charge duration s taken from the scheduled flex cheaper hours config
numHours = self.getActualHDayChargeTime(dayName)
if numHours:
numHours = numHours/3600

if numHours:
self.debugLog(10,"TWCMaster","numHours: "+str(numHours))
cheapestStartHour = self.getCheapestStartHour(numHours,startHour,endHour)
self.debugLog(10,"TWCMaster","cheapestStartHour: "+str(cheapestStartHour))
startHour = cheapestStartHour
endHour = startHour+numHours
if endHour >= 24:
endHour = endHour - 24

return (startHour, endHour, daysBitmap)

def getScheduledAmpsTimeFlex(self):
startHour = self.getScheduledAmpsStartHour()
days = self.getScheduledAmpsDaysBitmap()
Expand Down Expand Up @@ -325,7 +386,10 @@ def getScheduledAmpsTimeFlex(self):
# (if starting usually at 9pm and it calculates to start at 4am - it's already the next day)
if startHour < self.getScheduledAmpsDaysBitmap():
days = self.rotl(days, 7)
return (startHour, self.getScheduledAmpsEndHour(), days)

timeSettings = self.getScheduledAmpsCheaperFlex(startHour, self.getScheduledAmpsEndHour(), days)

return timeSettings

def getScheduledAmpsEndHour(self):
return self.settings.get("scheduledAmpsEndHour", -1)
Expand Down