From c7a94abae0f3053eff280229a612d23ee4d33577 Mon Sep 17 00:00:00 2001 From: JoshuaDodds Date: Wed, 14 Jun 2023 11:29:36 +0200 Subject: [PATCH] feature: add automatic switching to grid usage when energy free or negative cost, add automatic excess energy export/sale at the most profitable hour of each day --- lib/constants.py | 10 +++++---- lib/energy_broker.py | 44 ++++++++++++++++++++++++++++++++++++++ lib/event_handler.py | 33 +++++++++++----------------- lib/victron_integration.py | 6 +++++- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/lib/constants.py b/lib/constants.py index 93535ce..af98f16 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -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", diff --git a/lib/energy_broker.py b/lib/energy_broker.py index 5d12a6a..978ae8b 100644 --- a/lib/energy_broker.py +++ b/lib/energy_broker.py @@ -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...") @@ -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__) @@ -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) diff --git a/lib/event_handler.py b/lib/event_handler.py index 5af7252..8f3b9c5 100644 --- a/lib/event_handler.py +++ b/lib/event_handler.py @@ -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() @@ -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: @@ -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 @@ -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) diff --git a/lib/victron_integration.py b/lib/victron_integration.py index 83c9743..9ba79ee 100644 --- a/lib/victron_integration.py +++ b/lib/victron_integration.py @@ -2,9 +2,12 @@ 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')) @@ -12,7 +15,8 @@ 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):