Skip to content

Commit

Permalink
feature: add automatic switching to grid usage when energy free or ne…
Browse files Browse the repository at this point in the history
…gative cost, add automatic excess energy export/sale at the most profitable hour of each day
  • Loading branch information
JoshuaDodds committed Jun 14, 2023
1 parent ee52a3b commit c7a94ab
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 26 deletions.
10 changes: 6 additions & 4 deletions lib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ def dotenv_config(env_variable):
"system_shutdown": f"Cerbomoticzgx/system/shutdown",

# Tibber
"tibber_total": f"N/{systemId0}/Tibber/home/energy/day/euro_day_total", # workaround to update dz
"tibber_day_total": f"Tibber/home/energy/day/reward",
"tibber_last_update": f"Tibber/home/energy/day/last_update",
"tibber_price_now": f"Tibber/home/price_info/now/total",
"tibber_total": f"N/{systemId0}/Tibber/home/energy/day/euro_day_total", # workaround to update dz
"tibber_day_total": f"Tibber/home/energy/day/reward",
"tibber_last_update": f"Tibber/home/energy/day/last_update",
"tibber_price_now": f"Tibber/home/price_info/now/total",
"tibber_cost_highest_today": f"Tibber/home/price_info/today/highest/0/cost",
"tibber_cost_lowest_today": f"Tibber/home/price_info/today/lowest/0/cost",

# Tesla specific metrics
"tesla_power": f"N/{systemId0}/acload/40/Ac/Power",
Expand Down
44 changes: 44 additions & 0 deletions lib/energy_broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
from lib.notifications import pushover_notification
from lib.tibber_api import publish_pricing_data
from lib.global_state import GlobalStateClient
from lib.victron_integration import ac_power_setpoint


MAX_TIBBER_BUY_PRICE = float(dotenv_config('MAX_TIBBER_BUY_PRICE')) or None
STATE = GlobalStateClient()


def main():
logging.info("EnergyBroker: Initializing...")

Expand All @@ -26,6 +29,10 @@ def main():
def scheduler_loop():
def is_alive():
logging.info(f"EnergyBroker: heartbeat...thumpThump!")

# updating the hourly price is normally triggered on batter_soc changes. Since this will not happen when
# battery is full and idle, we check for that condition here and still update the hourly price at 5 minute
# intervals.
if dotenv_config('TIBBER_UPDATES_ENABLED') == '1' and STATE.get('batt_soc') == 100.0:
publish_pricing_data(__name__)

Expand All @@ -40,6 +47,43 @@ def is_alive():
scheduler.run_pending()
time.sleep(1)

def manage_sale_of_stored_energy_to_the_grid(batt_soc: float) -> None:
tibber_price_now = STATE.get('tibber_price_now')
tibber_24h_high = STATE.get('tibber_cost_highest_today')
ac_setpoint = STATE.get('ac_power_setpoint')

if batt_soc > 80.0 and tibber_price_now >= tibber_24h_high and tibber_price_now != 0:
if ac_setpoint != -10000.0:
ac_power_setpoint(watts="-10000.0")
logging.info(f"Beginning to sell energy at a cost of {round(tibber_price_now, 3)}")
pushover_notification("Energy Sale Alert",
f"Beginning to sell energy at a cost of {round(tibber_price_now, 3)}")
else:
if ac_setpoint < 0.0:
ac_power_setpoint(watts="0.0")
logging.info(f"Stopped energy export at {batt_soc} and a current price of {round(tibber_price_now, 3)}")
pushover_notification("Energy Sale Alert",
f"Stopped energy export at {batt_soc} and a current price of {round(tibber_price_now, 3)}")


def manage_grid_usage_based_on_current_price(price: float) -> None:
inverter_mode = int(STATE.get("inverter_mode"))

# if energy is free or the provider is paying, switch to using the grid and start vehicle charging
if price <= 0.0001 and inverter_mode == 3:
pushover_notification("Tibber Price Alert",
f"Energy cost is {round(price, 3)} cents per kWh. Switching to grid energy.")
Utils.set_inverter_mode(mode=1)
return

# revese the above action when energy is no longer free
if price >= 0.0001 and inverter_mode == 1:
print(inverter_mode)
pushover_notification("Tibber Price Alert",
f"Energy cost is {round(price, 3)} cents per kWh. Switching back to battery.")
Utils.set_inverter_mode(mode=3)
return

def publish_mqtt_trigger():
publish.single("Cerbomoticzgx/EnergyBroker/RunTrigger", payload=f"{{\"value\": {time.localtime().tm_hour}}}", qos=0, retain=False,
hostname=cerboGxEndpoint)
Expand Down
33 changes: 12 additions & 21 deletions lib/event_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
from lib.constants import dotenv_config, logging, cerboGxEndpoint
from lib.victron_integration import regulate_battery_max_voltage
from lib.tibber_api import publish_pricing_data
from lib.energy_broker import set_48h_charging_schedule, Utils
from lib.global_state import GlobalStateClient
from lib.notifications import pushover_notification
from lib.tesla_api import TeslaApi
from lib.energy_broker import (
manage_sale_of_stored_energy_to_the_grid,
set_48h_charging_schedule,
manage_grid_usage_based_on_current_price
)


tesla = TeslaApi()

Expand All @@ -33,12 +37,11 @@ def __init__(self, mqtt_topic, value, logging_msg=None):

def dispatch(self):
try:
# Update Global State db even if the method is not handled explicitly within the scope of
# this event handler.
self.gs_client.set(self.topic_key, self.value)

if self.topic_key:
# call the method which matches self.topic_key
# Update the Global State db even if we do not have an explicit method defined for this topi_key
self.gs_client.set(self.topic_key, self.value)

# if a method is defined, call it. Otherwise, call _unhandled_method()
getattr(self, self.topic_key, self._unhandled_method)()
logging.debug(f"{self.topic_key} method")
else:
Expand All @@ -53,20 +56,7 @@ def _unhandled_method(self):

def tibber_price_now(self):
_value = float(self.value)
inverter_mode = int(self.gs_client.get("inverter_mode"))
vehicle_is_charging = bool(self.gs_client.get("tesla_is_charging"))
vehicle_is_plugged = bool(self.gs_client.get("tesla_plug_status"))

# if energy is free or the provider is paying, switch to using the grid and start vehicle charging
if _value <= 0.0001 and inverter_mode == 3:
pushover_notification("Tibber Price Alert", f"Energy cost is {round(_value, 3)} cents per kWh. Switching to grid energy.")
Utils.set_inverter_mode(mode=1)

# revese the above action
if _value >= 0.0001 and inverter_mode == 1:
print(inverter_mode)
pushover_notification("Tibber Price Alert", f"Energy cost is {round(_value, 3)} cents per kWh. Switching back to battery.")
Utils.set_inverter_mode(mode=3)
manage_grid_usage_based_on_current_price(_value)

def system_shutdown(self):
_value = self.value
Expand All @@ -93,6 +83,7 @@ def batt_soc(self):
regulate_battery_max_voltage(_value)
if dotenv_config('TIBBER_UPDATES_ENABLED') == '1':
publish_pricing_data(__name__)
manage_sale_of_stored_energy_to_the_grid(_value)

def batt_power(self):
_value = round(self.value)
Expand Down
6 changes: 5 additions & 1 deletion lib/victron_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
import paho.mqtt.publish as publish
import paho.mqtt.subscribe as subscribe

from lib.global_state import GlobalStateClient

from lib.constants import logging, cerboGxEndpoint, Topics, TopicsWritable, dotenv_config


STATE = GlobalStateClient()
float_voltage = float(dotenv_config('BATTERY_FLOAT_VOLTAGE'))
max_voltage = float(dotenv_config('BATTERY_ABSORPTION_VOLTAGE'))
battery_full_voltage = float(dotenv_config('BATTERY_FULL_VOLTAGE'))

def ac_power_setpoint(watts=None):
if watts:
_msg = f"{{\"value\": {watts}}}"
logging.info(f"Victron Integration: Setting AC Power Set Point to: {watts} watts")
logging.debug(f"Victron Integration: Setting AC Power Set Point to: {watts} watts")
STATE.set(key='ac_power_setpoint', value="0.0")
publish.single(TopicsWritable['system0']['ac_power_setpoint'], payload=_msg, qos=0, retain=False, hostname=cerboGxEndpoint, port=1883)

def minimum_ess_soc(percent: int = 10):
Expand Down

0 comments on commit c7a94ab

Please sign in to comment.