Skip to content

Commit

Permalink
Merge pull request #384 from RichieB2B/max-power
Browse files Browse the repository at this point in the history
New feature that allows to limit the max power from the Grid
  • Loading branch information
ngardiner authored Dec 11, 2024
2 parents 6776af5 + 66aa05a commit 59da892
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 3 deletions.
45 changes: 45 additions & 0 deletions etc/twcmanager/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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":[
Expand Down
2 changes: 1 addition & 1 deletion lib/TWCManager/Control/themes/Default/jsrefresh.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion lib/TWCManager/Control/themes/Modern/jsrefresh.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 34 additions & 0 deletions lib/TWCManager/TWCManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -359,13 +371,35 @@ 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.
if master.getModuleByName("Policy").policyIsGreen():
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
Expand Down
68 changes: 67 additions & 1 deletion lib/TWCManager/TWCMaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ class TWCMaster:
backgroundTasksDelayed = []
config = None
consumptionValues = {}
consumptionAmpsValues = {}
debugOutputToFile = False
generationValues = {}
lastMaxAmpsToDivideFromGrid = 0
lastkWhMessage = time.time()
lastkWhPoll = 0
lastSaveFailed = 0
lastTWCResponseMsg = None
lastUpdateCheck = 0
masterTWCID = ""
maxAmpsToDivideAmongSlaves = 0
maxAmpsToDivideFromGrid = 0
modules = {}
nextHistorySnap = 0
overrideMasterHeartbeatData = b""
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 "
Expand All @@ -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()
Expand All @@ -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

Expand Down

0 comments on commit 59da892

Please sign in to comment.