diff --git a/etc/twcmanager/config.json b/etc/twcmanager/config.json index e0bf9189..e8f8766d 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": 16, + + # 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 @@ -323,6 +349,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":[ diff --git a/lib/TWCManager/Control/themes/Default/jsrefresh.html.j2 b/lib/TWCManager/Control/themes/Default/jsrefresh.html.j2 index d3c3716e..f3a2d3c6 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/Control/themes/Modern/jsrefresh.html.j2 b/lib/TWCManager/Control/themes/Modern/jsrefresh.html.j2 index 19464ee8..7d544285 100644 --- a/lib/TWCManager/Control/themes/Modern/jsrefresh.html.j2 +++ b/lib/TWCManager/Control/themes/Modern/jsrefresh.html.j2 @@ -48,7 +48,7 @@ $('#surplusAmps').html(surplusAmps.toFixed(2)); // 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") { if ($("#start_chargenow").length) { $("#start_chargenow").html("Update Charge Now"); $("#cancel_chargenow").prop("disabled", false); diff --git a/lib/TWCManager/TWCManager.py b/lib/TWCManager/TWCManager.py index debc2f6d..b8edef44 100755 --- a/lib/TWCManager/TWCManager.py +++ b/lib/TWCManager/TWCManager.py @@ -168,6 +168,16 @@ logging.getLogger().setLevel(logLevel) + + + +######################################################################## +# 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. @@ -328,6 +338,8 @@ def background_tasks_thread(master): master.saveSettings() elif task["cmd"] == "sunrise": update_sunrise_sunset() + elif task["cmd"] == "checkMaxPowerFromGrid": + check_max_power_from_grid() except: logger.info( @@ -359,6 +371,8 @@ def check_green_energy(): # Poll all loaded EMS modules for consumption and generation values for module in master.getModulesByType("EMS"): master.setConsumption(module["name"], module["ref"].getConsumption()) + if hasattr(module["ref"], "getConsumptionAmps"): + master.setConsumptionAmps(module["name"], module["ref"].getConsumptionAmps()) master.setGeneration(module["name"], module["ref"].getGeneration()) # Set max amps iff charge_amps isn't specified on the policy. @@ -366,6 +380,26 @@ 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()) + if hasattr(module["ref"], "getConsumptionAmps"): + master.setConsumptionAmps(module["name"], module["ref"].getConsumptionAmps()) + 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 # generation and consumption figures diff --git a/lib/TWCManager/TWCMaster.py b/lib/TWCManager/TWCMaster.py index cdcf94aa..8ef04df0 100644 --- a/lib/TWCManager/TWCMaster.py +++ b/lib/TWCManager/TWCMaster.py @@ -25,8 +25,10 @@ class TWCMaster: backgroundTasksDelayed = [] config = None consumptionValues = {} + consumptionAmpsValues = {} debugOutputToFile = False generationValues = {} + lastMaxAmpsToDivideFromGrid = 0 lastkWhMessage = time.time() lastkWhPoll = 0 lastSaveFailed = 0 @@ -34,6 +36,7 @@ class TWCMaster: lastUpdateCheck = 0 masterTWCID = "" maxAmpsToDivideAmongSlaves = 0 + maxAmpsToDivideFromGrid = 0 modules = {} nextHistorySnap = 0 overrideMasterHeartbeatData = b"" @@ -583,6 +586,17 @@ def getConsumption(self): return float(consumptionVal) + def getConsumptionAmps(self): + consumptionAmpsVal = 0 + + for key in self.consumptionAmpsValues: + consumptionAmpsVal += float(self.consumptionAmpsValues[key]) + + if consumptionAmpsVal < 0: + consumptionAmpsVal = 0 + + return float(consumptionAmpsVal) + def getFakeTWCID(self): return self.TWCID @@ -654,6 +668,39 @@ def getMaxAmpsToDivideGreenEnergy(self): amps = max(min(newOffer, solarAmps / self.getRealPowerFactor(solarAmps)), 0) return round(amps, 2) + def getMaxAmpsToDivideFromGrid(self): + # Only recalculate once every 30 seconds to allow things to settle + now = time.time() + if now - self.lastMaxAmpsToDivideFromGrid < 30: + logger.debug(f"getMaxAmpsToDivideFromGrid returns cashed value {self.maxAmpsToDivideFromGrid}") + return self.maxAmpsToDivideFromGrid + + currentOffer = self.getTotalAmpsInUse() if self.getTotalAmpsInUse() > 0 else self.getMaxAmpsToDivideAmongSlaves() + + # Get consumptions in Amps, if the EMS source supports it + consumptionA = float(self.getConsumptionAmps()) + + # Use convertWattsToAmps() if consumptionA is not available + if not consumptionA: + # Calculate our current generation and consumption in watts + consumptionW = float(self.getConsumption()) + generationW = float(self.getGeneration()) + consumptionA = self.convertWattsToAmps(consumptionW - generationW) + + # Calculate what we should max offer to align with max grid energy + maxAmpsAllowedFromGrid = self.config["config"].get("maxAmpsAllowedFromGrid", 16) + amps = maxAmpsAllowedFromGrid - consumptionA + currentOffer + if consumptionA > maxAmpsAllowedFromGrid: + logger.info(f"getMaxAmpsToDivideFromGrid limited power: consumption {consumptionA:.1f}A > {maxAmpsAllowedFromGrid}A") + amps = amps / self.getRealPowerFactor(amps) + logger.debug("MaxAmpsToDivideFromGrid: +++++++++++++++: " + str(amps)) + + # Update time for comparing next time + self.lastMaxAmpsToDivideFromGrid = now + + 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)] @@ -1285,6 +1332,9 @@ def setConsumption(self, source, value): # average across sources perhaps, or do a primary/secondary priority self.consumptionValues[source] = value + def setConsumptionAmps(self, source, value): + self.consumptionAmpsValues[source] = value + def setGeneration(self, source, value): self.generationValues[source] = value @@ -1315,7 +1365,7 @@ def setMaxAmpsToDivideAmongSlaves(self, amps): if amps > self.config["config"]["wiringMaxAmpsAllTWCs"]: # Never tell the slaves to draw more amps than the physical charger # wiring can handle. - logger.info( + logger.error( "ERROR: specified maxAmpsToDivideAmongSlaves " + str(amps) + " > wiringMaxAmpsAllTWCs " @@ -1324,6 +1374,17 @@ def setMaxAmpsToDivideAmongSlaves(self, amps): ) amps = self.config["config"]["wiringMaxAmpsAllTWCs"] + activePolicy=str(self.getModuleByName("Policy").active_policy) + if (activePolicy == "Charge Now with Grid power limit" or \ + activePolicy == "Scheduled Charging with Grid power limit") and \ + amps > self.maxAmpsToDivideFromGrid: + # Never tell the slaves to draw more amps from grid than allowed + amps = self.maxAmpsToDivideFromGrid + logger.info( + "maxAmpsToDivideAmongSlaves limited to not draw more power from the grid than allowed: " + str(amps) + ) + + self.maxAmpsToDivideAmongSlaves = amps self.releaseBackgroundTasksLock() @@ -1332,6 +1393,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