diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd0eb0c7..853a2845 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,8 @@ This document logs the changes per release of TWCManager.
* Expose all time properties to the policy module for evaluation (thanks @MikeBishop)
* Impovements to policy page in Web UI to show the value of policy parameters (thanks @MikeBishop)
* Move grace period functionality for vehicles connected prior to policy evaluation to the master module, which opens the door to policy evaluation based on vehicle arrival/VIN (thanks @MikeBishop)
+ * Split and show the values of Charger Load and Other Load in console output when the Subtract Charger Load setting is enabled (thanks @mikey4321)
+ * Added EMS module support for SmartMe API
* Bugfixes
* Add a sleep of 5 seconds when waking car up to avoid an infinite loop (thanks @dschuesae)
* Fix a bug with the legacy web interface which causes the Resume Track Green Energy setting of None to fail. Also added a deprecation notice to the web interface to ensure people don't inadvertently use it over the modular interface.
diff --git a/README.md b/README.md
index 0dd2bc3d..daf40451 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
## Screenshots
![Screenshot](docs/screenshot.png)
-![Screenshot](docs/screenshot2.png)
+![Screenshot](docs/screenshot3.png)
## How it works
diff --git a/TWCManager.py b/TWCManager.py
index 38389f1d..bab41ac8 100755
--- a/TWCManager.py
+++ b/TWCManager.py
@@ -74,6 +74,7 @@
"Status.MQTTStatus",
"Pricing.aWATTarPricing",
"Pricing.StaticPricing",
+ "Pricing.PVPCesPricing",
]
# Enable support for Python Visual Studio Debugger
@@ -99,6 +100,10 @@
debugLog(1, "Unable to find a configuration file.")
sys.exit()
+
+
+
+########################################################################
# All TWCs ship with a random two-byte TWCID. We default to using 0x7777 as our
# fake TWC ID. There is a 1 in 64535 chance that this ID will match each real
# TWC on the network, in which case you should pick a different random id below.
@@ -242,6 +247,7 @@ def background_tasks_thread(master):
elif task["cmd"] == "saveSettings":
master.saveSettings()
+
except:
master.debugLog(
1,
@@ -288,7 +294,6 @@ def check_green_energy():
master.setGeneration(module["name"], module["ref"].getGeneration())
master.setMaxAmpsToDivideAmongSlaves(master.getMaxAmpsToDivideGreenEnergy())
-
def update_statuses():
# Print a status update if we are on track green energy showing the
diff --git a/docs/Settings.md b/docs/Settings.md
index 731f1335..f5f1f6d0 100644
--- a/docs/Settings.md
+++ b/docs/Settings.md
@@ -37,3 +37,15 @@ The Do not Charge action states that outside of scheduled or Track Green Energy
* Track Green Energy
This is an option that currently does not operate (and will only set Non-Scheduled Charging rate to 0). In future, this will allow continuing of Track Green Energy behaviour outside of the hard-coded daylight hours (6am - 8pm).
+
+### Manual Tesla API key override
+
+In some instances, you may prefer to obtain the Tesla API keys yourself. The main benefit of this approach is that you do not need to provide your Tesla username or password to TWCManager.
+
+Another reason to use this feature might be as a temporary workaround if the Tesla authentication flow is changed or the TWCManager authentication function is faulty.
+
+Note: Providing your Tesla username and password to TWCManager to automatically fetch your Tesla API access and refresh tokens does not put your credentials at significant risk as they are only used once to fetch the token before being destroyed, however there may nonetheless be a preference not to provide these credentials at all.
+
+To obtain the key, you will need some knowledge of the Tesla API authentication flow. To assist with this, a link to a service which can assist you with this process is provided, however this does therefore require you to provide your credentials to that service. Otherwise, you may want to research the Tesla authentication flow and obtain the tokens yourself, or to obtain them from another application that you have previously authenticated to.
+
+Providing any value for the Access or Refresh tokens will result in the current stored tokens being overridden with the value you supply. We don't perform any validation of the tokens and the previous values are lost. Back up your settings.json file prior to entering your token manually if you need to revert your settings.
diff --git a/docs/screenshot3.png b/docs/screenshot3.png
new file mode 100644
index 00000000..a373884b
Binary files /dev/null and b/docs/screenshot3.png differ
diff --git a/etc/twcmanager/config.json b/etc/twcmanager/config.json
index 6f659596..3aaf807d 100644
--- a/etc/twcmanager/config.json
+++ b/etc/twcmanager/config.json
@@ -32,6 +32,32 @@
# wiringMaxAmpsPerTWC = 50 * 0.8 = 40 and wiringMaxAmpsAllTWCs = 40 + 40 = 80.
"wiringMaxAmpsPerTWC": 6,
+
+ # If you what to limit the power drawn from the Grid you need to set this
+ # maxAmpsAllowedFromGrid and extend the policy you what it to apply, i.e.:
+ # { "name": "Charge Now with Grid power limit",
+ # "match": [
+ # "settings.chargeNowAmps",
+ # "settings.chargeNowTimeEnd",
+ # "settings.chargeNowTimeEnd",
+ # ],
+ # "condition": ["gt", "gt", "gt"],
+ # "value": [0, 0, "now"],
+ # "background_task": "checkMaxPowerFromGrid",
+ # "charge_amps": "settings.chargeNowAmps",
+ # "charge_limit": "config.chargeNowLimit"},
+
+ # { "name": "Scheduled Charging with Grid power limit",
+ # "match": [ "checkScheduledCharging()" ],
+ # "condition": [ "eq" ],
+ # "value": [ 1 ],
+ # "background_task": "checkMaxPowerFromGrid",
+ # "charge_amps": "settings.scheduledAmpsMax",
+ # "charge_limit": "config.scheduledLimit"},
+
+ "maxAmpsAllowedFromGrid": 15,
+
+
# https://teslamotorsclub.com/tmc/threads/model-s-gen2-charger-efficiency-testing.78740/#post-1844789
# says you're using 10.85% more power (91.75/82.77=1.1085) charging at 5A vs 40A,
# 2.48% more power at 10A vs 40A, and 1.9% more power at 20A vs 40A. This is
@@ -258,6 +284,25 @@
#
# They should primarily be used to abort charging when necessary.
"emergency":[
+ { "name": "Charge Now with Grid power limit",
+ "match": [
+ "settings.chargeNowAmps",
+ "settings.chargeNowTimeEnd",
+ "settings.chargeNowTimeEnd",
+ ],
+ "condition": ["gt", "gt", "gt"],
+ "value": [0, 0, "now"],
+ "background_task": "checkMaxPowerFromGrid",
+ "charge_amps": "settings.chargeNowAmps",
+ "charge_limit": "config.chargeNowLimit"},
+
+ { "name": "Scheduled Charging with Grid power limit",
+ "match": [ "checkScheduledCharging()" ],
+ "condition": [ "eq" ],
+ "value": [ 1 ],
+ "background_task": "checkMaxPowerFromGrid",
+ "charge_amps": "settings.scheduledAmpsMax",
+ "charge_limit": "config.scheduledLimit"},
],
# Rules in the before section here are evaluated after the Charge Now rule
"before":[
@@ -423,7 +468,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
diff --git a/html/index.php b/html/index.php
index 7e07326c..82bc8cb6 100755
--- a/html/index.php
+++ b/html/index.php
@@ -7,10 +7,6 @@
// 1 is just the most useful info.
// 10 is all info.
$debugLevel = 0;
-
- // Point $twcScriptDir to the directory containing the TWCManager.py script.
- // Interprocess Communication with TWCManager.py will not work if this
- // parameter is incorrect.
$twcScriptDir = "/etc/twcmanager";
// End configuration parameters
diff --git a/lib/TWCManager/Control/HTTPControl.py b/lib/TWCManager/Control/HTTPControl.py
index 416703b6..84b62024 100644
--- a/lib/TWCManager/Control/HTTPControl.py
+++ b/lib/TWCManager/Control/HTTPControl.py
@@ -54,6 +54,7 @@ def __init__(self, master):
def CreateHTTPHandlerClass(master):
class HTTPControlHandler(BaseHTTPRequestHandler):
ampsList = []
+ kwhList = []
fields = {}
hoursDurationList = []
master = None
@@ -73,6 +74,12 @@ def __init__(self, *args, **kwargs):
for amp in range(5, (master.config["config"].get("wiringMaxAmpsPerTWC", 5)) + 1):
self.ampsList.append([amp, str(amp) + "A"])
+ # Populate kwhList so that any function which requires a list of supported
+ # TWC kwh can easily access it
+ if not len(self.kwhList):
+ for kwh in range(40, 140):
+ self.kwhList.append([kwh, str(kwh) + "Kwh"])
+
# Populate list of hours
if not len(self.hoursDurationList):
for hour in range(1, 25):
@@ -104,6 +111,7 @@ def __init__(self, *args, **kwargs):
# render HTML, we can keep using those even inside jinja2
self.templateEnv.globals.update(addButton=self.addButton)
self.templateEnv.globals.update(ampsList=self.ampsList)
+ self.templateEnv.globals.update(kwhList=self.kwhList)
self.templateEnv.globals.update(chargeScheduleDay=self.chargeScheduleDay)
self.templateEnv.globals.update(doChargeSchedule=self.do_chargeSchedule)
self.templateEnv.globals.update(hoursDurationList=self.hoursDurationList)
@@ -126,8 +134,18 @@ def checkBox(self, name, value):
return cb
def do_chargeSchedule(self):
+ # For future days we use the name of the day with the suffix "next"
schedule = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]
settings = master.settings.get("Schedule", {})
+ pricesI = master.getWeekImportPrice()
+ if not pricesI:
+ pricesI={}
+ ltNow = time.localtime()
+ hourNow = ltNow.tm_hour
+ if ltNow.tm_wday<6:
+ wdayNow = ltNow.tm_wday+1
+ else:
+ wdayNow = 0
page = """
@@ -139,20 +157,55 @@ def do_chargeSchedule(self):
page += """
"""
- for i in (x for y in (range(6, 24), range(0, 6)) for x in y):
+ for i in (x for y in (range(0, 8), range(8, 24)) for x in y):
page += "
%02d
" % (i)
- for day in schedule:
+ for dayTn in range(0,7):
+ day=schedule[dayTn]
+
+ energyOffset = int(master.queryGreenEnergyWhDay(day,i))
+ ampsOffset = round(master.convertWattsToAmps(energyOffset),2)
+ futureColor=""
+ if pricesI.get("next"+day,None) != None:
+ day = "next"+day
+ futureColor = ";color:blue"
+ if dayTn == wdayNow and i >= hourNow:
+ futureColor = ";color:blue"
+
+ if dayTn>0:
+ dayYn=schedule[dayTn-1]
+ else:
+ dayYn=schedule[6]
today = settings.get(day, {})
+ yesterday = settings.get(dayYn, {})
curday = settings.get("Common", {})
if (settings.get("schedulePerDay", 0)):
curday = settings.get(day, {})
- if (today.get("enabled", None) == "on" and
- (int(curday.get("start", 0)[:2]) <= int(i)) and
- (int(curday.get("end", 0)[:2]) >= int(i))):
- page += "
SC @ " + str(settings.get("Settings", {}).get("scheduledAmpsMax", 0)) + "A
"
- else:
+ start = int(curday.get("start", "24:00")[:2])
+ end = int(curday.get("end", "24:00")[:2])
+
+ price=0
+ if pricesI.get(day,None) != None:
+ price = pricesI[day][str(i)]
+
+ if ((today.get("enabled", None) == "on" and (
+ (start < end and start <= i and end > i) or (start > end and start <= i)))
+ or (yesterday.get("enabled", None) == "on" and start > end and i < end)):
+ if price <= 0 and ampsOffset == 0 :
+ page += "
SC@" + str(settings.get("Settings", {}).get("scheduledAmpsMax", 0)) + "A
"
+ elif ampsOffset == 0 :
+ page += "
SC@" + str(settings.get("Settings", {}).get("scheduledAmpsMax", 0)) + "A "+str(price)+"€
"
+ else:
+ page += "
SC@" + str(settings.get("Settings", {}).get("scheduledAmpsMax", 0)) + "A "+str(price)+"€ "+str(ampsOffset)+"A
"
+
+ else :
#Todo - need to mark track green + non scheduled chg
- page += "
"
+ if price <= 0 and ampsOffset == 0 :
+ page += "
"
+ elif ampsOffset == 0 :
+ page += "
"+str(price)+"€
"
+ else:
+ page += "
"+str(price)+"€ "+str(ampsOffset)+"A
"
+
page += "
"
page += ""
page += "
"
@@ -504,10 +557,17 @@ def do_GET(self):
self.do_API_GET()
return
+ if self.url.path == "/teslaAccount/login":
+ # For security, these details should be submitted via a POST request
+ # Send a 405 Method Not Allowed in response.
+ self.send_response(405)
+ page = "This function may only be requested via the POST HTTP method."
+ self.wfile.write(page.encode("utf-8"))
+ return
+
if (
self.url.path == "/"
- or self.url.path == "/apiacct/True"
- or self.url.path == "/apiacct/False"
+ or self.url.path.startswith("/teslaAccount")
):
self.send_response(200)
self.send_header("Content-type", "text/html")
@@ -578,14 +638,38 @@ def do_GET(self):
self.wfile.write(page.encode("utf-8"))
return
- if self.url.path == "/tesla-login":
- # For security, these details should be submitted via a POST request
- # Send a 405 Method Not Allowed in response.
- self.send_response(405)
- page = "This function may only be requested via the POST HTTP method."
+ if self.url.path == "/graphs" or self.url.path == "/graphsP":
+ # We query the last 24h by default
+ now = datetime.now().replace(second=0, microsecond=0)
+ initial=now - timedelta(hours=24)
+ end= now
+ # It we came from a POST the dates should be already stored in settings
+ if self.url.path == "/graphs":
+ self.process_save_graphs(initial,end)
+
+ self.send_response(200)
+ self.send_header("Content-type", "text/html")
+ self.end_headers()
+ # Load debug template and render
+ self.template = self.templateEnv.get_template("nographs.html.j2")
+ for module in master.getModulesByType("Logging"):
+ if module["ref"].greenEnergyQueryAvailable():
+ self.template = self.templateEnv.get_template("graphs.html.j2")
+ break
+
+ page = self.template.render(self.__dict__)
self.wfile.write(page.encode("utf-8"))
return
+ if self.url.path == "/graphs/date":
+ inicio=master.settings["Graphs"]["Initial"]
+ fin=master.settings["Graphs"]["End"]
+
+ self.process_graphs(inicio,fin)
+ return
+
+
+
# All other routes missed, return 404
self.send_response(404)
@@ -616,12 +700,25 @@ def do_POST(self):
self.process_save_settings()
return
- if self.url.path == "/tesla-login":
+ if self.url.path == "/teslaAccount/login":
# User has submitted Tesla login.
# Pass it to the dedicated process_teslalogin function
self.process_teslalogin()
return
+ if self.url.path == "/graphs/dates":
+ # User has submitted dates to graph this period.
+ objIni = datetime.strptime(self.getFieldValue("dateIni"), "%Y-%m-%dT%H:%M")
+ objEnd = datetime.strptime(self.getFieldValue("dateEnd"), "%Y-%m-%dT%H:%M")
+ self.process_save_graphs(objIni,objEnd)
+ self.send_response(302)
+ self.send_header("Location", "/graphsP")
+ self.end_headers()
+
+ self.wfile.write("".encode("utf-8"))
+ return
+
+
# All other routes missed, return 404
self.send_response(404)
self.end_headers()
@@ -651,16 +748,29 @@ def chargeScheduleDay(self, day):
page += "
"
return page
@@ -709,11 +819,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'(?Penabled|end|flex|start)(?P.*?)ChargeTime')
+ ct = re.compile(r'(?Penabled|end|flex|cheaper|actualH|start)(?P.*?)ChargeTime')
for key in self.fields:
match = ct.match(key)
if match:
@@ -745,6 +857,9 @@ def process_save_schedule(self):
master.settings["scheduledAmpsStartHour"] = int(master.settings["Schedule"]["Common"]["start"][:2])
master.settings["scheduledAmpsEndHour"] = int(master.settings["Schedule"]["Common"]["end"][:2])
master.settings["scheduledAmpsMax"] = float(master.settings["Schedule"]["Settings"]["scheduledAmpsMax"])
+ master.settings["flexBatterySize"] = float(master.settings["Schedule"]["Settings"]["flexBatterySize"])
+ master.debugLog(10,"HTTP","scheduledAmpsMax: "+str(master.settings["scheduledAmpsMax"]))
+ master.debugLog(10,"HTTP","flexBatterySize: "+str(master.settings["flexBatterySize"]))
# Scheduled Days bitmap backward compatibility
master.settings["scheduledAmpsDaysBitmap"] = (
@@ -768,8 +883,22 @@ def process_save_schedule(self):
def process_save_settings(self):
- # Write settings
+ # This function will write the settings submitted from the settings
+ # page to the settings dict, before triggering a write of the settings
+ # to file
for key in self.fields:
+
+ # If the key relates to the car API tokens, we need to pass these
+ # to the appropriate module, rather than directly updating the
+ # configuration file (as it would just be overwritten)
+ if (key == "carApiBearerToken" or key == "carApiRefreshToken") and self.getFieldValue(key) != "":
+ carapi = master.getModuleByName("TeslaAPI")
+ if key == "carApiBearerToken":
+ carapi.setCarApiBearerToken(self.getFieldValue(key))
+ elif key == "carApiRefreshToken":
+ carapi.setCarApiRefreshToken(self.getFieldValue(key))
+
+ # Write setting to dictionary
master.settings[key] = self.getFieldValue(key)
# If Non-Scheduled power action is either Do not Charge or
@@ -810,7 +939,7 @@ def process_teslalogin(self):
# Redirect to an index page with output based on the return state of
# the function
self.send_response(302)
- self.send_header("Location", "/apiacct/" + str(ret))
+ self.send_header("Location", "/teslaAccount/" + str(ret))
self.end_headers()
self.wfile.write("".encode("utf-8"))
return
@@ -880,6 +1009,71 @@ def show_twcs(self):
page += ""
return page
+ def process_save_graphs(self,initial,end):
+ # Check that Graphs dict exists within settings.
+ # If not, this would indicate that this is the first time
+ # we have saved it
+ if (master.settings.get("Graphs", None) == None):
+ master.settings["Graphs"] = {}
+ master.settings["Graphs"]["Initial"]=initial
+ master.settings["Graphs"]["End"]=end
+
+ return
+
+ def process_graphs(self,init,end):
+ # This function will query the green_energy SQL table
+ result={}
+ try:
+ available=False
+ for module in master.getModulesByType("Logging"):
+ if module["ref"].greenEnergyQueryAvailable():
+ result= module["ref"].queryGreenEnergy(
+ {
+ "dateBegin": init,
+ "dateEnd": end
+ }
+ )
+ available = True
+ break
+ if not available:
+ return
+
+ except Exception as e:
+ master.debugLog(1,
+ "HTTPCtrl",
+ "Excepcion queryGreenEnergy: "
+ + e,
+ )
+
+ data = {}
+ data[0] = {
+ "initial":init.strftime("%Y-%m-%dT%H:%M"),
+ "end":end.strftime("%Y-%m-%dT%H:%M"),
+ }
+ i=1
+ while i
+
+
+
+
diff --git a/lib/TWCManager/Control/themes/Default/graphs.html.j2 b/lib/TWCManager/Control/themes/Default/graphs.html.j2
new file mode 100644
index 00000000..8da8d4cc
--- /dev/null
+++ b/lib/TWCManager/Control/themes/Default/graphs.html.j2
@@ -0,0 +1,20 @@
+
+
+
+ TWCManager
+ {% include 'bootstrap.html.j2' %}
+ {% include 'drawChart.html.j2' %}
+
+
+ {% include 'navbar.html.j2' %}
+
+
+
+
+
+
diff --git a/lib/TWCManager/Control/themes/Default/main.html.j2 b/lib/TWCManager/Control/themes/Default/main.html.j2
index c493a47b..208a08c2 100644
--- a/lib/TWCManager/Control/themes/Default/main.html.j2
+++ b/lib/TWCManager/Control/themes/Default/main.html.j2
@@ -10,21 +10,37 @@
- {% if url.path == "/apiacct/False" %}
+ {% if url.path == "/teslaAccount/False" %}
Failed to log in to Tesla Account. Please check username and password and try again.
+ {% elif url.path == "/teslaAccount/MFA" %}
+
+ Tesla MFA account login is not yet available, sorry! Check back shortly.
+
+ {% elif url.path == "/teslaAccount/Phase1Error" %}
+
+ Error encountered during Phase 1 (GET) of the Tesla Authentication process.
+
+ {% elif url.path == "/teslaAccount/Phase2Error" or url.path == "/teslaAccount/Phase2ErrorTip" %}
+
+ Error encountered during Phase 2 (POST) of the Tesla Authentication process.
+ {% if url.path == "/teslaAccount/Phase2ErrorTip" %}
+
TIP: If login fails at this point, it could be due to a locked Tesla account from too many login attempts, even if the last attempt was the correct password. Try logging out and then into your Tesla account to verify
+ {% endif %}
+
{% endif %}
{% if not master.teslaLoginAskLater
- and url.path != "/apiacct/True" %}
+ and url.path != "/teslaAccount/True"
+ and url.path != "/teslaAccount/MFA" %}
{% if not apiAvailable %}
{% include 'request_teslalogin.html.j2' %}
{% endif %}
{% endif %}
- {% if url.path == "/apiacct/True" %}
+ {% if url.path == "/teslaAccount/True" %}
Thank you, successfully fetched Tesla API token.
{% endif %}
diff --git a/lib/TWCManager/Control/themes/Default/navbar.html.j2 b/lib/TWCManager/Control/themes/Default/navbar.html.j2
index 3897f611..a441ae97 100644
--- a/lib/TWCManager/Control/themes/Default/navbar.html.j2
+++ b/lib/TWCManager/Control/themes/Default/navbar.html.j2
@@ -11,6 +11,7 @@
{{ navbarItem("/schedule", "Schedule")|safe }}
{{ navbarItem("/settings", "Settings")|safe }}
{{ navbarItem("/debug", "Debug")|safe }}
+ {{ navbarItem("/graphs", "Graphs")|safe }}
{{ navbarItem("https://github.com/ngardiner/TWCManager", "GitHub")|safe }}
v{{ master.version }}
diff --git a/lib/TWCManager/Control/themes/Default/nographs.html.j2 b/lib/TWCManager/Control/themes/Default/nographs.html.j2
new file mode 100644
index 00000000..4566f369
--- /dev/null
+++ b/lib/TWCManager/Control/themes/Default/nographs.html.j2
@@ -0,0 +1,11 @@
+
+
+
+ TWCManager
+ {% include 'bootstrap.html.j2' %}
+
+
+ {% include 'navbar.html.j2' %}
+ You need to activate a Logging module that allows to get historical green energy information (MySQL)
+
+
diff --git a/lib/TWCManager/Control/themes/Default/request_teslalogin.html.j2 b/lib/TWCManager/Control/themes/Default/request_teslalogin.html.j2
index 2ab03dde..0d0ed655 100644
--- a/lib/TWCManager/Control/themes/Default/request_teslalogin.html.j2
+++ b/lib/TWCManager/Control/themes/Default/request_teslalogin.html.j2
@@ -1,4 +1,4 @@
-