From 1dc8277117c9ba1dd7483434d98983779da0b78e Mon Sep 17 00:00:00 2001 From: Paul Elsner Date: Sat, 4 Oct 2025 21:20:45 +0200 Subject: [PATCH] feat: used information about planed car charging to build load profile with this any planned charging is added to the profile. The information that charging will happen exist so we can use it to build a more precise prediction. For simplicity the amount needed energy is evenly distributed over the charging hours. This is wrong if for example the late-charge option is set so that there is a pause between start and finish of the charging session. --- src/eos_connect.py | 1 + src/interfaces/evcc_interface.py | 21 +++----------- src/interfaces/load_interface.py | 50 ++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/eos_connect.py b/src/eos_connect.py index a8620ba..201f898 100644 --- a/src/eos_connect.py +++ b/src/eos_connect.py @@ -237,6 +237,7 @@ def mqtt_control_callback(command): load_interface = LoadInterface( config_manager.config.get("load", {}), time_zone, + evcc_interface, ) battery_interface = BatteryInterface( diff --git a/src/interfaces/evcc_interface.py b/src/interfaces/evcc_interface.py index f40a9e8..b34d72c 100644 --- a/src/interfaces/evcc_interface.py +++ b/src/interfaces/evcc_interface.py @@ -87,23 +87,6 @@ def __init__( self.last_known_charging_state = False # off, pv, pvmin, now self.last_known_charging_mode = None - self.current_detail_data_list = [ - { - "connected": False, - "charging": False, - "mode": "off", - "chargeDuration": 0, - "chargeRemainingDuration": 0, - "chargedEnergy": 0, - "chargeRemainingEnergy": 0, - "sessionEnergy": 0, - "vehicleSoc": 0, - "vehicleRange": 0, - "vehicleOdometer": 0, - "vehicleName": "", - "smartCostActive": False, - } - ] self.external_battery_mode_en = ext_bat_mode self.external_battery_mode = "off" # Default mode @@ -212,6 +195,8 @@ def __get_default_detail_data(self): "vehicleOdometer": 0, "vehicleName": "", "smartCostActive": False, + "planProjectedStart": "", + "planProjectedEnd": "", } ] @@ -418,6 +403,8 @@ def __get_states_of_loadpoints(self, loadpoints, vehicles): "vehicleOdometer": loadpoint.get("vehicleOdometer", 0), "vehicleName": vehicle_name, "smartCostActive": loadpoint.get("smartCostActive", False), + "planProjectedStart": loadpoint.get("planProjectedStart", ""), + "planProjectedEnd": loadpoint.get("planProjectedEnd", ""), } self.current_detail_data_list.append(detail_data) return True diff --git a/src/interfaces/load_interface.py b/src/interfaces/load_interface.py index 32ab3a7..5bf0eee 100644 --- a/src/interfaces/load_interface.py +++ b/src/interfaces/load_interface.py @@ -11,6 +11,8 @@ import requests import pytz +from interfaces.evcc_interface import EvccInterface + logger = logging.getLogger("__main__") logger.info("[LOAD-IF] loading module ") @@ -26,7 +28,9 @@ def __init__( self, config, timezone=None, # Changed default to None + evcc_interface: EvccInterface = None, ): + self.evcc_interface = evcc_interface self.src = config.get("source", "") self.url = config.get("url", "") self.load_sensor = config.get("load_sensor", "") @@ -56,6 +60,26 @@ def __init__( self.__check_config() + def __parse_iso_time_timezone(self, time_str): + time = datetime.fromisoformat(time_str) + + # If no timezone is configured, return the time as-is + if self.time_zone is None: + return time + + if time.tzinfo is None: + # If datetime is naive, localize it + if hasattr(self.time_zone, 'localize'): + # pytz timezone object + time = self.time_zone.localize(time) + else: + # zoneinfo.ZoneInfo object + time = time.replace(tzinfo=self.time_zone) + else: + # Convert to configured timezone + time = time.astimezone(self.time_zone) + return time + def __check_config(self): """ Checks if the configuration is valid. @@ -615,6 +639,32 @@ def __create_load_profile_weekdays(self): + " will improve with collected data" ) + if self.evcc_interface is not None: + # use projected charge start and end time to calculate car load + evcc_detail = self.evcc_interface.get_current_detail_data() + try: + charge_start = self.__parse_iso_time_timezone(evcc_detail[0]["planProjectedStart"]) + charge_end = self.__parse_iso_time_timezone(evcc_detail[0]["planProjectedEnd"]) + charge_amount = evcc_detail[0].get("chargeRemainingEnergy", 0) + charge_duration = ((charge_end - charge_start).total_seconds() + 3599) / 3600 + charge_per_hour = charge_amount / charge_duration if charge_duration > 0 else 0 + day_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + start_index = int((charge_start - day_start).total_seconds() / 3600) + end_index = start_index + int(charge_duration) + for i in range(start_index, end_index): + if 0 <= i < len(load_profile): + load_profile[i] += charge_per_hour + logger.debug( + "[LOAD-IF] Adding projected EV charge load of %5.1f Wh at hour index %d", + charge_per_hour, i + ) + logger.info( + "[LOAD-IF] Adjusted load profile for projected EV charging from %s to %s", + charge_start, charge_end + ) + except: + pass + logger.debug("[LOAD-IF] Load profile values : %s", load_profile) return load_profile def get_load_profile(self, tgt_duration, start_time=None):