Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/CONFIG_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 24 additions & 5 deletions src/eos_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -726,19 +738,26 @@ 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": {
"current_ac_charge_demand": current_ac_charge_demand,
"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")

Expand Down
38 changes: 33 additions & 5 deletions src/interfaces/base_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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"
Expand Down
115 changes: 75 additions & 40 deletions src/interfaces/evcc_interface.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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():
Expand All @@ -111,31 +129,48 @@ 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.
"""
data = self.fetch_evcc_state_via_api()
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):
"""
Expand All @@ -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"
Expand Down
Loading