diff --git a/TWCManager.py b/TWCManager.py
index 63b76607..5dfba154 100755
--- a/TWCManager.py
+++ b/TWCManager.py
@@ -70,6 +70,7 @@
"EMS.SolarLog",
"EMS.TeslaPowerwall2",
"EMS.TED",
+ "EMS.Efergy",
"Status.HASSStatus",
"Status.MQTTStatus",
]
@@ -97,6 +98,16 @@
debugLog(1, "Unable to find a configuration file.")
sys.exit()
+
+
+
+########################################################################
+# Write the PID in order to let a supervisor restart it in case of crash
+PIDfile=config["config"]["settingsPath"] + "/TWCManager.pid"
+PIDTWCManager=open(PIDfile,"w")
+PIDTWCManager.write(str(os.getpid()))
+PIDTWCManager.close()
+
# 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.
@@ -237,6 +248,9 @@ def background_tasks_thread(master):
requests.post(task["url"], json=body)
elif task["cmd"] == "saveSettings":
master.saveSettings()
+ elif task["cmd"] == "checkMaxPowerFromGrid":
+ check_max_power_from_grid()
+
except:
master.debugLog(
@@ -285,6 +299,24 @@ def check_green_energy():
master.setMaxAmpsToDivideAmongSlaves(master.getMaxAmpsToDivideGreenEnergy())
+def check_max_power_from_grid():
+ global config, hass, master
+
+ # Check solar panel generation using an API exposed by
+ # the HomeAssistant API.
+ #
+ # You may need to customize the sensor entity_id values
+ # to match those used in your environment. This is configured
+ # in the config section at the top of this file.
+ #
+ # Poll all loaded EMS modules for consumption and generation values
+ for module in master.getModulesByType("EMS"):
+ master.setConsumption(module["name"], module["ref"].getConsumption())
+ master.setGeneration(module["name"], module["ref"].getGeneration())
+ master.setMaxAmpsToDivideFromGrid(master.getMaxAmpsToDivideFromGrid())
+
+
+
def update_statuses():
# Print a status update if we are on track green energy showing the
diff --git a/etc/twcmanager/config.json b/etc/twcmanager/config.json
index 755f7969..4c007cca 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":[
@@ -495,7 +540,13 @@
"generationItem": "Generation item name",
"serverIP": "192.168.1.2",
"serverPort": "8080"
+ },
+ # The Efergy server allows fetching of consumption from https://engage.efergy.com/ token is needed
+ "Efergy": {
+ "enabled": false,
+ "token": "xx"
}
+
},
# Status plugins allow us to export status detail out of TWCManager.
diff --git a/lib/TWCManager/Control/HTTPControl.py b/lib/TWCManager/Control/HTTPControl.py
index 18bff965..cd29163e 100644
--- a/lib/TWCManager/Control/HTTPControl.py
+++ b/lib/TWCManager/Control/HTTPControl.py
@@ -148,7 +148,7 @@ def do_chargeSchedule(self):
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))):
+ (int(curday.get("end", 0)[:2]) > int(i))):
page += "
SC @ " + str(settings.get("Settings", {}).get("scheduledAmpsMax", 0)) + "A | "
else:
#Todo - need to mark track green + non scheduled chg
diff --git a/lib/TWCManager/Control/themes/Default/jsrefresh.html.j2 b/lib/TWCManager/Control/themes/Default/jsrefresh.html.j2
index eaa93f10..2523259e 100644
--- a/lib/TWCManager/Control/themes/Default/jsrefresh.html.j2
+++ b/lib/TWCManager/Control/themes/Default/jsrefresh.html.j2
@@ -20,7 +20,7 @@ $(document).ready(function() {
}
// Change the state of the Charge Now button based on Charge Policy
- if (json["currentPolicy"] == "Charge Now") {
+ if (json["currentPolicy"] == "Charge Now" || json["currentPolicy"] == "Charge Now with Grid power limit") {
document.getElementById("start_chargenow").value = "Update Charge Now";
document.getElementById("cancel_chargenow").disabled = false;
} else {
diff --git a/lib/TWCManager/EMS/Efergy.py b/lib/TWCManager/EMS/Efergy.py
new file mode 100644
index 00000000..1bbbd042
--- /dev/null
+++ b/lib/TWCManager/EMS/Efergy.py
@@ -0,0 +1,124 @@
+# Efergy
+
+
+class Efergy:
+
+ import requests
+ import time
+
+ cacheTime = 10
+ config = None
+ configConfig = None
+ configEfergy = None
+ consumedW = 0
+ debugLevel = 0
+ fetchFailed = False
+ token=0
+ generatedW = 0
+ importW = 0
+ exportW = 0
+ lastFetch = 0
+ master = None
+ status = False
+ timeout = 10
+ voltage = 0
+
+ def __init__(self, master):
+ self.master = master
+ self.config = master.config
+ try:
+ self.configConfig = master.config["config"]
+ except KeyError:
+ self.configConfig = {}
+ try:
+ self.configEfergy = master.config["sources"]["Efergy"]
+ except KeyError:
+ self.configEfergy = {}
+ self.debugLevel = self.configConfig.get("debugLevel", 0)
+ self.status = self.configEfergy.get("enabled", False)
+ self.token = self.configEfergy.get("token", None)
+ self.serverPort = self.configEfergy.get("serverPort", "80")
+
+ # Unload if this module is disabled or misconfigured
+ if (not self.status):
+ self.master.releaseModule("lib.TWCManager.EMS", "Efergy")
+ return None
+
+ def debugLog(self, minlevel, message):
+ self.master.debugLog(minlevel, "Efergy", message)
+
+ def getConsumption(self):
+
+ if not self.status:
+ self.debugLog(10, "Efergy EMS Module Disabled. Skipping getConsumption")
+ return 0
+
+ # Perform updates if necessary
+ self.update()
+
+ # Return consumption value
+ return float(self.consumedW)
+
+ def getGeneration(self):
+
+ if not self.status:
+ self.debugLog(10, "Efergy EMS Module Disabled. Skipping getGeneration")
+ return 0
+
+ # Perform updates if necessary
+ self.update()
+
+ # Return generation value
+ if not self.generatedW:
+ self.generatedW = 0
+ return float(self.generatedW)
+
+
+ def getValue(self, url):
+
+ # Fetch the specified URL from the Efergy and return the data
+ self.fetchFailed = False
+
+ try:
+ r = self.requests.get(url, timeout=self.timeout)
+ except self.requests.exceptions.ConnectionError as e:
+ self.debugLog(
+ 4, "Error connecting to Efergy to fetch sensor value"
+ )
+ self.debugLog(10, str(e))
+ self.fetchFailed = True
+ return False
+
+ r.raise_for_status()
+ jsondata = r.json()
+ return jsondata
+
+ def getMeterData(self):
+ url = "https://engage.efergy.com/mobile_proxy/getCurrentValuesSummary?token="+self.token
+
+ return self.getValue(url)
+
+ def update(self):
+
+ if (int(self.time.time()) - self.lastFetch) > self.cacheTime:
+ # Cache has expired. Fetch values from Efergy.
+
+ meterData = self.getMeterData()
+
+ if meterData:
+ try:
+ self.consumedW = list(meterData[0]['data'][0].values())[0]
+ except (KeyError, TypeError) as e:
+ self.debugLog(
+ 4, "Exception during parsing Meter Data (Consumption)"
+ )
+ self.debugLog(10, e)
+
+ # Update last fetch time
+ if self.fetchFailed is not True:
+ self.lastFetch = int(self.time.time())
+
+ return True
+ else:
+ # Cache time has not elapsed since last fetch, serve from cache.
+ return False
diff --git a/lib/TWCManager/TWCMaster.py b/lib/TWCManager/TWCMaster.py
index a2c7cbb0..7f167c9a 100644
--- a/lib/TWCManager/TWCMaster.py
+++ b/lib/TWCManager/TWCMaster.py
@@ -32,6 +32,7 @@ class TWCMaster:
lastTWCResponseMsg = None
masterTWCID = ""
maxAmpsToDivideAmongSlaves = 0
+ maxAmpsToDivideFromGrid = 0
modules = {}
nextHistorySnap = 0
overrideMasterHeartbeatData = b""
@@ -532,6 +533,29 @@ def getMaxAmpsToDivideGreenEnergy(self):
amps = amps / self.getRealPowerFactor(amps)
return round(amps, 2)
+ def getMaxAmpsToDivideFromGrid(self):
+ # Calculate our current generation and consumption in watts
+ generationW = float(self.getGeneration())
+ consumptionW = float(self.getConsumption())
+
+ currentOffer = min(
+ self.getTotalAmpsInUse(),
+ self.getMaxAmpsToDivideAmongSlaves(),
+ )
+
+ # Calculate what we should max offer to align with max grid energy
+ amps = self.config["config"]["maxAmpsAllowedFromGrid"] + \
+ self.convertWattsToAmps(generationW - consumptionW) + \
+ currentOffer
+
+ amps = amps / self.getRealPowerFactor(amps)
+ self.debugLog(
+ 10, "TWCMaster", "MaxAmpsToDivideFromGrid: +++++++++++++++: " + str(amps)
+ )
+
+ return round(amps, 2)
+
+
def getNormalChargeLimit(self, ID):
if "chargeLimits" in self.settings and str(ID) in self.settings["chargeLimits"]:
result = self.settings["chargeLimits"][str(ID)]
@@ -1167,6 +1191,15 @@ def setMaxAmpsToDivideAmongSlaves(self, amps):
)
amps = self.config["config"]["wiringMaxAmpsAllTWCs"]
+
+ if amps > self.maxAmpsToDivideFromGrid:
+ # Never tell the slaves to draw more amps from grid than allowed
+ amps = self.maxAmpsToDivideFromGrid
+ self.debugLog(
+ 10, "TWCMaster","maxAmpsToDivideAmongSlaves limited to not draw more power from the grid than allowed: " + str(amps)
+ )
+
+
self.maxAmpsToDivideAmongSlaves = amps
self.releaseBackgroundTasksLock()
@@ -1175,6 +1208,11 @@ def setMaxAmpsToDivideAmongSlaves(self, amps):
# to console / MQTT / etc
self.queue_background_task({"cmd": "updateStatus"})
+ def setMaxAmpsToDivideFromGrid(self, amps):
+ # This is called when check_max_power_from_grid is run
+ # It stablished how much power we allow getting from the grid
+ self.maxAmpsToDivideFromGrid = amps
+
def setNonScheduledAmpsMax(self, amps):
self.settings["nonScheduledAmpsMax"] = amps
diff --git a/svisorTWC.sh b/svisorTWC.sh
new file mode 100755
index 00000000..dba0c589
--- /dev/null
+++ b/svisorTWC.sh
@@ -0,0 +1,24 @@
+PROGRAM=/usr/bin/python3.5
+PIDFILE=/etc/twcmanager/TWCManager.pid
+TWCMANAGER_PATH=/home/pi/TWCManager
+
+while true
+do
+
+if [ -f $PIDFILE ]; then
+ read PID <$PIDFILE
+ echo $PID
+ if [ -d /proc/$PID ] && [ "$(readlink -f /proc/$PID/exe)" = "$PROGRAM" ]; then
+ echo "done."
+ else
+ echo "PID not found, Starting..."
+ screen -dm -S TWCManager $TWCMANAGER_PATH/TWCManager.py
+ fi
+else
+ echo "PID file not found "; echo $PIDFILE; echo ", Starting..."
+ screen -dm -S TWCManager $TWCMANAGER_PATH/TWCManager.py
+fi
+sleep 30
+done
+
+