diff --git a/src/CONFIG_README.md b/src/CONFIG_README.md index 31d4a6a4..9c204206 100644 --- a/src/CONFIG_README.md +++ b/src/CONFIG_README.md @@ -87,7 +87,7 @@ Feel free to add more PV forecast entries under the pv_forecast section by provi ### Inverter Configuration Settings -- **inverter.type**: default: fronius_gen24 - currently not used +- **inverter.type**: fronius_gen24 or default - **inverter.address**: address of the inverter - e.g. 192.168.1.12 - **inverter.user**: username in local portal e.g. customer (inverter local webpage login data - be aware: website Customer vs. customer in this level) - **inverter.password**: password for local portal diff --git a/src/eos_connect.py b/src/eos_connect.py index b294eb7e..6467df6a 100644 --- a/src/eos_connect.py +++ b/src/eos_connect.py @@ -113,6 +113,8 @@ def charging_state_callback(new_state): """ # update the base control with the new charging state base_control.set_current_evcc_charging_state(evcc_interface.get_charging_state()) + base_control.set_current_evcc_charging_mode(evcc_interface.get_charging_mode()) + logger.info("[MAIN] EVCC Event - Charging state changed to: %s", new_state) change_control_state() @@ -579,6 +581,7 @@ def setting_control_data(ac_charge_demand_rel, dc_charge_demand_rel, discharge_a base_control.set_current_battery_soc(battery_interface.get_current_soc()) # getting the current charging state from evcc base_control.set_current_evcc_charging_state(evcc_interface.get_charging_state()) + base_control.set_current_evcc_charging_mode(evcc_interface.get_charging_mode()) def change_control_state(): @@ -642,7 +645,7 @@ def change_control_state(): base_control.get_current_overall_state(), "unknown state" ), ) - # MODE_AVOID_DISCHARGE_EVCC + # MODE_AVOID_DISCHARGE_EVCC_FAST elif base_control.get_current_overall_state() == 3: if inverter_en: inverter_interface.set_mode_avoid_discharge() @@ -652,6 +655,15 @@ def change_control_state(): base_control.get_current_overall_state(), "unknown state" ), ) + elif base_control.get_current_overall_state() == 4: + if inverter_en: + inverter_interface.set_mode_allow_discharge() + logger.info( + "[Main] Inverter mode set to %s (_____-+-+-_____)", + base_control.get_state_mapping().get( + base_control.get_current_overall_state(), "unknown state" + ), + ) elif base_control.get_current_overall_state() < 0: logger.warning("[Main] Inverter mode not initialized yet") return True @@ -726,6 +738,7 @@ def get_controls(): current_battery_soc = battery_interface.get_current_soc() base_control.set_current_battery_soc(current_battery_soc) current_inverter_mode = base_control.get_current_overall_state(False) + current_inverter_mode_num = base_control.get_current_overall_state() response_data = { "current_states": { @@ -733,12 +746,18 @@ def get_controls(): "current_dc_charge_demand": current_dc_charge_demand, "current_discharge_allowed": current_discharge_allowed, "inverter_mode": current_inverter_mode, - "evcc_charging_state": base_control.get_current_evcc_charging_state(), + "inverter_mode_num": current_inverter_mode_num, + }, + "evcc": { + "charging_state": base_control.get_current_evcc_charging_state(), + "charging_mode": base_control.get_current_evcc_charging_mode(), + }, + "battery": { + "soc": current_battery_soc, + "max_charge_power_dyn": battery_interface.get_max_charge_power_dyn(), }, - "battery_soc": current_battery_soc, - "battery_max_charge_power_dyn": battery_interface.get_max_charge_power_dyn(), - "timestamp": datetime.now(time_zone).isoformat(), "state": optimization_scheduler.get_current_state(), + "timestamp": datetime.now(time_zone).isoformat(), } return Response(json.dumps(response_data, indent=4), content_type="application/json") diff --git a/src/interfaces/base_control.py b/src/interfaces/base_control.py index 1e82ff31..d77cf19a 100644 --- a/src/interfaces/base_control.py +++ b/src/interfaces/base_control.py @@ -14,15 +14,18 @@ MODE_CHARGE_FROM_GRID = 0 MODE_AVOID_DISCHARGE = 1 MODE_DISCHARGE_ALLOWED = 2 -MODE_AVOID_DISCHARGE_EVCC = 3 +MODE_AVOID_DISCHARGE_EVCC_FAST = 3 +MODE_DISCHARGE_ALLOWED_EVCC_PV = 4 state_mapping = { 0: "MODE_CHARGE_FROM_GRID", 1: "MODE_AVOID_DISCHARGE", 2: "MODE_DISCHARGE_ALLOWED", - 3: "MODE_AVOID_DISCHARGE_EVCC", + 3: "MODE_AVOID_DISCHARGE_EVCC_FAST", + 4: "MODE_DISCHARGE_ALLOWED_EVCC_PV", } + class BaseControl: """ BaseControl is a class that manages the state and demands of a control system. @@ -37,6 +40,7 @@ def __init__(self, config, timezone): self.current_dc_charge_demand = 0 self.current_discharge_allowed = 1 self.current_evcc_charging_state = False + self.current_evcc_charging_mode = False # startup with None to force a writing to the inverter self.current_overall_state = None self.current_battery_soc = 0 @@ -91,6 +95,12 @@ def get_current_overall_state(self, number=True): # Return the string representation of the state return state_mapping.get(self.current_overall_state, "unknown state") + def get_current_overall_state_number(self): + """ + Returns the current overall state as a number. + """ + return self.current_overall_state + def get_current_battery_soc(self): """ Returns the current battery state of charge (SOC). @@ -103,6 +113,12 @@ def get_current_evcc_charging_state(self): """ return self.current_evcc_charging_state + def get_current_evcc_charging_mode(self): + """ + Returns the current EVCC charging mode. + """ + return self.current_evcc_charging_mode + def set_current_ac_charge_demand(self, value_relative): """ Sets the current AC charge demand. @@ -158,6 +174,14 @@ def set_current_evcc_charging_state(self, value): # logger.debug("[BASE_CTRL] set current EVCC charging state to %s", value) self.set_current_overall_state() + def set_current_evcc_charging_mode(self, value): + """ + Sets the current EVCC charging mode. + """ + self.current_evcc_charging_mode = value + # logger.debug("[BASE_CTRL] set current EVCC charging mode to %s", value) + self.set_current_overall_state() + def set_current_overall_state(self): """ Sets the current overall state and logs the timestamp if it changes. @@ -173,9 +197,13 @@ def set_current_overall_state(self): self.current_ac_charge_demand != self.last_ac_charge_demand ) - # override overall state if EVCC charging state is active and discharge is allowed - if new_state == MODE_DISCHARGE_ALLOWED and self.current_evcc_charging_state: - new_state = MODE_AVOID_DISCHARGE_EVCC + # override overall state if EVCC charging state is active and in mode fast charge and discharge is allowed + if ( + new_state == MODE_DISCHARGE_ALLOWED + and self.current_evcc_charging_state + and self.current_evcc_charging_mode == "now" + ): + new_state = MODE_AVOID_DISCHARGE_EVCC_FAST logger.info( "[BASE_CTRL] EVCC charging state is active," + " setting overall state to MODE_AVOID_DISCHARGE" diff --git a/src/interfaces/evcc_interface.py b/src/interfaces/evcc_interface.py index e81e4cef..9e57bed2 100644 --- a/src/interfaces/evcc_interface.py +++ b/src/interfaces/evcc_interface.py @@ -1,21 +1,26 @@ -''' -This module provides the `EvccInterface` class, which serves as an interface to interact -with the Electric Vehicle Charging Controller (EVCC) API. The class enables periodic -fetching of the charging state and triggers a callback when the state changes. +""" +This module provides the `EvccInterface` class, which serves as an interface to interact +with the Electric Vehicle Charging Controller (EVCC) API. The class enables periodic +fetching of the charging state and charging mode, and triggers a callback when either +state changes. + Classes: - EvccInterface: A class to interact with the EVCC API, manage charging state updates, - and handle state change callbacks. + EvccInterface: A class to interact with the EVCC API, manage charging state and mode + updates, and handle state change callbacks. + Dependencies: - logging: For logging messages and errors. - threading: For managing background threads. - time: For implementing delays in the update loop. - requests: For making HTTP requests to the EVCC API. + Usage: - Create an instance of the `EvccInterface` class by providing the EVCC API URL, - an optional update interval, and a callback function to handle charging state changes. - The class will automatically start a background thread to periodically fetch the - charging state from the API. -''' + Create an instance of the `EvccInterface` class by providing the EVCC API URL, + an optional update interval, and a callback function to handle charging state or + mode changes. The class will automatically start a background thread to periodically + fetch the charging state and mode from the API. +""" + import logging import threading import time @@ -26,27 +31,33 @@ class EvccInterface: - ''' - EvccInterface is a class that provides an interface to interact with the EVCC - (Electric Vehicle Charging Controller) API. - It periodically fetches the charging state and triggers a callback when the state changes. - Attributes: - last_known_charging_state (bool): The last known charging state. - on_charging_state_change (callable): A callback function to be called when - the charging state changes. - _update_thread (threading.Thread): The background thread for updating - the charging state. - _stop_event (threading.Event): An event to signal the thread to stop. - Methods: - __init__(url, update_interval=15, on_charging_state_change=None): - get_charging_state(): - start_update_service(): - shutdown(): - _update_charging_state_loop(): - request_charging_state(): - Fetches the EVCC state from the API and updates the charging state. - fetch_evcc_state_via_api(): - ''' + """ + EvccInterface is a class that provides an interface to interact with the EVCC + (Electric Vehicle Charging Controller) API. + It periodically fetches the charging state and mode, and triggers a callback when + either the state or mode changes. + + Attributes: + last_known_charging_state (bool): The last known charging state. + last_known_charging_mode (str): The last known charging mode. + on_charging_state_change (callable): A callback function to be called when + the charging state or mode changes. + _update_thread (threading.Thread): The background thread for updating + the charging state and mode. + _stop_event (threading.Event): An event to signal the thread to stop. + + Methods: + __init__(url, update_interval=15, on_charging_state_change=None): + get_charging_state(): + get_charging_mode(): + start_update_service(): + shutdown(): + _update_charging_state_loop(): + __request_charging_state(): + Fetches the EVCC state from the API and updates the charging state and mode. + fetch_evcc_state_via_api(): + """ + def __init__(self, url, update_interval=15, on_charging_state_change=None): """ Initializes the EVCC interface and starts the update service. @@ -59,6 +70,8 @@ def __init__(self, url, update_interval=15, on_charging_state_change=None): """ self.url = url self.last_known_charging_state = False + # off, pv, pvmin, now + self.last_known_charging_mode = None self.update_interval = update_interval self.on_charging_state_change = on_charging_state_change # Store the callback self._update_thread = None @@ -70,6 +83,11 @@ def get_charging_state(self): Returns the last known charging state. """ return self.last_known_charging_state + def get_charging_mode(self): + """ + Returns the last known charging mode. + """ + return self.last_known_charging_mode def start_update_service(self): """ @@ -98,10 +116,10 @@ def _update_charging_state_loop(self): """ while not self._stop_event.is_set(): try: - self.request_charging_state() + self.__request_charging_state() except (requests.exceptions.RequestException, ValueError, KeyError) as e: logger.error("[EVCC] Error while updating charging state: %s", e) - # Break the sleep interval into smaller chunks to allow immediate shutdown + # Break the sleep interval into smaller chunks to allow immediate shutdown sleep_interval = self.update_interval while sleep_interval > 0: if self._stop_event.is_set(): @@ -111,7 +129,7 @@ def _update_charging_state_loop(self): self.start_update_service() - def request_charging_state(self): + def __request_charging_state(self): """ Fetches the EVCC state from the API and returns the charging state. """ @@ -119,23 +137,40 @@ def request_charging_state(self): if not data or not isinstance(data.get("result", {}).get("loadpoints"), list): logger.error("[EVCC] Invalid or missing loadpoints in the response.") return None - loadpoint = data["result"]["loadpoints"][0] if data["result"]["loadpoints"] else None + loadpoint = ( + data["result"]["loadpoints"][0] if data["result"]["loadpoints"] else None + ) charging_state = loadpoint.get("charging") if loadpoint else None if not isinstance(charging_state, bool): logger.error("[EVCC] Charging state is not a valid boolean value.") return None - logger.debug("[EVCC] Charging state: %s", charging_state) - + # logger.debug("[EVCC] Charging state: %s", charging_state) # Check if the charging state has changed if charging_state != self.last_known_charging_state: logger.info("[EVCC] Charging state changed to: %s", charging_state) self.last_known_charging_state = charging_state + # Trigger the callback if provided + if self.on_charging_state_change: + self.on_charging_state_change(charging_state) + charging_mode = loadpoint.get("mode") if loadpoint else None + if charging_mode not in ["off", "pv", "minpv", "now"]: + logger.error( + "[EVCC] Charging mode is not a valid state." + + " Expected one of ['off', 'pv', 'minpv', 'now']. Got: %s", + charging_mode + ) + return None + logger.debug("[EVCC] Charging state: %s - Charging mode: %s", charging_mode, charging_state) + # Check if the charging state has changed + if charging_mode != self.last_known_charging_mode: + logger.info("[EVCC] Charging mode changed to: %s", charging_mode) + self.last_known_charging_mode = charging_mode # Trigger the callback if provided if self.on_charging_state_change: self.on_charging_state_change(charging_state) - return charging_state + return charging_state, charging_mode def fetch_evcc_state_via_api(self): """ @@ -147,7 +182,7 @@ def fetch_evcc_state_via_api(self): returns None. Returns: - dict: The JSON response from the EVCC API containing the state information, + dict: The JSON response from the EVCC API containing the state information, or None if the request fails or times out. """ evcc_url = self.url + "/api/state" diff --git a/src/web/index.html b/src/web/index.html index 827714cb..ad42d327 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -9,7 +9,8 @@ - + + @@ -31,7 +32,7 @@
Current Controls - --.--.-- +
@@ -76,10 +77,10 @@ - +
Dynamic Max Charge Power --
@@ -112,7 +113,10 @@
-
eCar Charging
+
eCar Charging + ... + ... +
@@ -155,7 +159,7 @@
-
Energy Consumption +
Energy Optimization 00:00:00 00:00:00
@@ -170,12 +174,15 @@
time
-
Discharge
-
AC Charge
-
Price
-
Expense
+
Control
target state
+
Price
ct/kWh
+
Expense
+
Income
+
+   +
Data 1
@@ -199,6 +206,8 @@