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 + +