Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9b5fded
Merge pull request #2039 from openWB/master
LKuemmel Nov 25, 2024
f717f28
Merge master into Release (#2050)
LKuemmel Dec 3, 2024
d1ccee6
Revert "Merge master into Release (#2050)"
LKuemmel Dec 5, 2024
b0420bd
Merge pull request #2057 from openWB/revert-2050-master
LKuemmel Dec 5, 2024
d982f63
Merge pull request #2058 from openWB/master
LKuemmel Dec 5, 2024
cb8fe8d
Merge pull request #2251 from openWB/master
LKuemmel Mar 11, 2025
31aef70
Merge pull request #2259 from openWB/master
LKuemmel Mar 14, 2025
8a8cefd
pro+: fix network setup (#2405)
LKuemmel May 16, 2025
cf7d39d
update ubuntu version to latest in github action (#2337)
LKuemmel Apr 16, 2025
503b475
fix keep cloud config on startup (#2406)
LKuemmel May 16, 2025
87108ed
Update version 2.1.7-Patch.2
LKuemmel May 16, 2025
87ffd48
Pro+: fix soc and mac (#2424)
LKuemmel May 28, 2025
8fd4fe2
Update version 2.1.7-Patch.3
LKuemmel May 28, 2025
f4e20ce
testpatch for Victron 3P75CT
AlexHuebi Jun 11, 2025
349d150
fix for unrealistic phase values
AlexHuebi Jun 11, 2025
f0f2049
fix power/current directions
AlexHuebi Jun 11, 2025
d7924dc
Pro+:RFID-Read plugged to Pi or Pro (#2408)
LKuemmel May 19, 2025
563e25d
update version 2.1.7-Patch.4
LKuemmel Jul 15, 2025
f346706
Merge pull request #2605 from benderl/remote-support
benderl Jul 29, 2025
2d946b8
Update version 2.1.7-Patch.5
LKuemmel Aug 1, 2025
4b0b2a7
Merge branch 'Release' into release-merge
ndrsnhs Sep 22, 2025
7ec28c6
Merge pull request #2779 from openWB/release-merge
ndrsnhs Sep 22, 2025
b2e5402
Merge branch 'Release' into release-merge-1
LKuemmel Sep 24, 2025
cffea69
Merge pull request #2782 from openWB/release-merge-1
LKuemmel Sep 24, 2025
90905f9
Merge remote-tracking branch 'origin/Release' into Victron_UDP_Patch
AlexHuebi Oct 10, 2025
9a4c420
Update version 2.1.9-Patch.1
LKuemmel Jan 28, 2026
2e6d083
New debug handler (#3103)
ndrsnhs Jan 28, 2026
90d7005
fix range calculation in case of error (#3108)
LKuemmel Jan 28, 2026
31c7ac7
modify log type for valid partner ids (#3110)
benderl Jan 28, 2026
2a73a43
write registers just once (#3115)
ndrsnhs Jan 29, 2026
3ba677d
add phase check (#3117)
ndrsnhs Jan 29, 2026
45ef9f5
set hysteresis_discharge (#3104)
ndrsnhs Jan 29, 2026
76dbc61
check endianness (#3105)
ndrsnhs Jan 29, 2026
3e1c1b8
build UI (#3120)
LKuemmel Jan 30, 2026
e8d38a8
reset set current in case of no charging and changing chargemode (#3124)
LKuemmel Feb 3, 2026
110372e
Update version 2.2.0-Alpha.1
LKuemmel Feb 5, 2026
2998b92
binary payload builder (#3125)
LKuemmel Feb 5, 2026
fceab86
remove duplicate code (#3121)
ndrsnhs Feb 5, 2026
d101b91
limit log size (#3096)
LKuemmel Feb 5, 2026
5051152
adjust currents, add powers and voltages (#3123)
ndrsnhs Feb 5, 2026
e827773
rse: handle undefinded patterns (#3074)
LKuemmel Feb 5, 2026
876c240
improve scheduled charging (#3127)
LKuemmel Feb 5, 2026
a2b1d3b
Undo fix f0f2049
AlexHuebi Feb 9, 2026
52cc874
Merge branch 'master' into Victron_UDP_Patch
AlexHuebi Feb 9, 2026
282b71b
fix version number
AlexHuebi Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion docs/samples/sample_modbus/sample_modbus/bat.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def set_power_limit(self, power_limit: Optional[int]) -> None:
# Wenn der Speicher die Steuerung der Ladeleistung unterstützt, muss bei Übergabe einer Zahl auf aktive
# Speichersteurung umgeschaltet werden, sodass der Speicher mit der übergebenen Leistung lädt/entlädt. Wird
# None übergeben, muss der Speicher die Null-Punkt-Ausregelung selbst übernehmen.
self.client.write_registers(reg, power_limit)
self.client.write_register(reg, power_limit)
# Wenn der Speicher keine Steuerung der Ladeleistung unterstützt
pass

Expand Down
26 changes: 18 additions & 8 deletions packages/control/ev/charge_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ class ChargeTemplate:
data: ChargeTemplateData = field(default_factory=charge_template_data_factory, metadata={
"topic": ""})

BUFFER = -1200 # nach mehr als 20 Min Überschreitung wird der Termin als verpasst angesehen
CHARGING_PRICE_EXCEEDED = ("Der aktuelle Strompreis liegt über dem maximalen Strompreis. ")
CHARGING_PRICE_LOW = "Laden, da der aktuelle Strompreis unter dem maximalen Strompreis liegt."

Expand Down Expand Up @@ -309,7 +308,8 @@ def eco_charging(self,
log.exception("Fehler im ev-Modul "+str(self.data.id))
return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), 0

BUFFER = -1200 # nach mehr als 20 Min Überschreitung wird der Termin als verpasst angesehen
BUFFER_AFTER_END_TIME = -1200 # nach mehr als 20 Min Überschreitung wird der Termin als verpasst angesehen
BUFFER_START_EARLIER = 600 # 10 Min vor dem geplanten Start kann begonnen werden

def _find_recent_plan(self,
plans: List[ScheduledChargingPlan],
Expand All @@ -330,7 +330,7 @@ def _find_recent_plan(self,
f"oder im Plan {p.name} als Begrenzung Energie einstellen.")
try:
plans_diff_end_date.append(
{p.id: timecheck.check_end_time(p, self.BUFFER)})
{p.id: timecheck.check_end_time(p, self.BUFFER_AFTER_END_TIME)})
log.debug(f"Verbleibende Zeit bis zum Zieltermin [s]: {plans_diff_end_date}")
except Exception:
log.exception("Fehler im ev-Modul "+str(self.data.id))
Expand All @@ -340,7 +340,7 @@ def _find_recent_plan(self,
if filtered_plans:
sorted_plans = sorted(filtered_plans, key=lambda x: list(x.values())[0])
for plan in sorted_plans:
if self.BUFFER < list(plan.values())[0]:
if self.BUFFER_AFTER_END_TIME < list(plan.values())[0]:
plan_dict = plan
break
else:
Expand Down Expand Up @@ -396,6 +396,7 @@ def scheduled_charging(self,
plan_data,
soc,
used_amount,
max_hw_phases,
control_parameter.phases,
control_parameter.min_current,
soc_request_interval_offset,
Expand Down Expand Up @@ -435,9 +436,15 @@ def _calc_remaining_time(self,
charging_type, ev_template, bidi)
phases = control_parameter_phases
remaining_time = plan_end_time - duration
elif plan.et_active:
duration, missing_amount = self._calculate_duration(
plan, soc, ev_template.data.battery_capacity, used_amount, max_hw_phases,
charging_type, ev_template, bidi)
phases = max_hw_phases
remaining_time = plan_end_time - duration
else:
duration_3p, missing_amount = self._calculate_duration(
plan, soc, ev_template.data.battery_capacity, used_amount, 3,
plan, soc, ev_template.data.battery_capacity, used_amount, max_hw_phases,
charging_type, ev_template, bidi)
remaining_time_3p = plan_end_time - duration_3p
duration_1p, missing_amount = self._calculate_duration(
Expand All @@ -450,7 +457,7 @@ def _calc_remaining_time(self,
# Zeit reicht nicht mehr für einphasiges Laden
remaining_time = remaining_time_3p
duration = duration_3p
phases = 3
phases = max_hw_phases
else:
remaining_time = remaining_time_1p
duration = duration_1p
Expand All @@ -459,7 +466,7 @@ def _calc_remaining_time(self,
elif plan.phases_to_use == 3 or plan.phases_to_use == 1:
duration, missing_amount = self._calculate_duration(
plan, soc, ev_template.data.battery_capacity,
used_amount, plan.phases_to_use, charging_type, ev_template, bidi)
used_amount, min(plan.phases_to_use, max_hw_phases), charging_type, ev_template, bidi)
remaining_time = plan_end_time - duration
phases = plan.phases_to_use

Expand Down Expand Up @@ -525,6 +532,7 @@ def scheduled_charging_calc_current(self,
selected_plan: Optional[SelectedPlan],
soc: int,
used_amount: float,
max_phases_hw: int,
control_parameter_phases: int,
min_current: int,
soc_request_interval_offset: int,
Expand Down Expand Up @@ -572,7 +580,8 @@ def scheduled_charging_calc_current(self,
phases = plan.phases_to_use_pv
elif limit.selected == "amount" and used_amount >= limit.amount:
message = self.SCHEDULED_CHARGING_REACHED_AMOUNT
elif 0 - soc_request_interval_offset < selected_plan.remaining_time < 300 + soc_request_interval_offset:
elif (0 - soc_request_interval_offset < selected_plan.remaining_time <
self.BUFFER_START_EARLIER + soc_request_interval_offset):
# Wenn der SoC ein paar Minuten alt ist, kann der Termin trotzdem gehalten werden.
# Zielladen kann nicht genauer arbeiten, als das Abfrageintervall vom SoC.
# 5 Min vor spätestem Ladestart
Expand Down Expand Up @@ -643,6 +652,7 @@ def convert_loading_hours_to_string(hour_list: List[int]) -> str:
if data.data.optional_data.ep_is_charging_allowed_hours_list(hour_list):
message = self.SCHEDULED_CHARGING_CHEAP_HOUR.format(get_hours_message())
current = plan_current
phases = max_phases_hw if plan.phases_to_use == 0 else plan.phases_to_use
submode = "instant_charging"
elif ((limit.selected == "soc" and soc <= limit.soc_limit) or
(limit.selected == "amount" and used_amount < limit.amount)):
Expand Down
12 changes: 6 additions & 6 deletions packages/control/ev/charge_template_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ def test_scheduled_charging_recent_plan(end_time_mock,
False, (16, "instant_charging",
ChargeTemplate.SCHEDULED_CHARGING_MAX_CURRENT.format(16), 3),
id="few minutes too late, but didn't miss for today"),
pytest.param(SelectedPlan(remaining_time=301, duration=3600), 79, 0, "soc",
False, (6, "pv_charging", ChargeTemplate.SCHEDULED_CHARGING_USE_PV.format("um 8:45 Uhr"), 0),
pytest.param(SelectedPlan(remaining_time=601, duration=3600), 79, 0, "soc",
False, (6, "pv_charging", ChargeTemplate.SCHEDULED_CHARGING_USE_PV.format("um 8:50 Uhr"), 0),
id="too early, use pv"),
])
def test_scheduled_charging_calc_current(plan_data: SelectedPlan,
Expand All @@ -275,7 +275,7 @@ def test_scheduled_charging_calc_current(plan_data: SelectedPlan,
plan_data.plan = plan

# execution
ret = ct.scheduled_charging_calc_current(plan_data, soc, used_amount, 3, 6,
ret = ct.scheduled_charging_calc_current(plan_data, soc, used_amount, 3, 3, 6,
0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE)

# evaluation
Expand All @@ -288,7 +288,7 @@ def test_scheduled_charging_calc_current_no_plans():

# execution
ret = ct.scheduled_charging_calc_current(
None, 63, 5, 3, 6, 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE)
None, 63, 5, 3, 3, 6, 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE)

# evaluation
assert ret == (0, "stop", ChargeTemplate.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED, 3)
Expand Down Expand Up @@ -382,8 +382,8 @@ def test_scheduled_charging_calc_current_electricity_tariff(

# execution
ret = ct.scheduled_charging_calc_current(
SelectedPlan(plan=plan, remaining_time=301, phases=3, duration=3600),
current_soc, 0, 3, 6, 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE)
SelectedPlan(plan=plan, remaining_time=601, phases=3, duration=3600),
current_soc, 0, 3, 3, 6, 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE)

# evaluation
assert ret == expected
8 changes: 5 additions & 3 deletions packages/control/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ def process_algorithm_results(self) -> None:
cp.initiate_phase_switch()
if control_parameter.state == ChargepointState.NO_CHARGING_ALLOWED and cp.data.set.current != 0:
control_parameter.state = ChargepointState.WAIT_FOR_USING_PHASES
self._update_state(cp)
cp.set_timestamp_charge_start()
else:
control_parameter.state = ChargepointState.NO_CHARGING_ALLOWED
self._update_state(cp)

if cp.data.get.state_str is not None:
Pub().pub("openWB/set/chargepoint/"+str(cp.num)+"/get/state_str",
cp.data.get.state_str)
Expand Down Expand Up @@ -135,8 +136,9 @@ def _update_state(self, chargepoint: chargepoint.Chargepoint) -> None:

chargepoint.data.set.current = current
Pub().pub("openWB/set/chargepoint/"+str(chargepoint.num)+"/set/current", current)
log.info(f"LP{chargepoint.num}: set current {current} A, "
f"state {ChargepointState(chargepoint.data.control_parameter.state).name}")
if chargepoint.data.get.plug_state:
log.info(f"LP{chargepoint.num}: set current {current} A, "
f"state {ChargepointState(chargepoint.data.control_parameter.state).name}")

def _start_charging(self, chargepoint: chargepoint.Chargepoint) -> Thread:
return Thread(target=chargepoint.chargepoint_module.set_current,
Expand Down
20 changes: 18 additions & 2 deletions packages/helpermodules/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,9 +730,25 @@ def removeVehicle(self, connection_id: str, payload: dict) -> None:
def sendDebug(self, connection_id: str, payload: dict) -> None:
pub_user_message(payload, connection_id, "Systembericht wird erstellt...", MessageType.INFO)
previous_log_level = SubData.system_data["system"].data["debug_level"]
create_debug_log(payload["data"])
json_rsp = create_debug_log(payload["data"])
Pub().pub("openWB/set/system/debug_level", previous_log_level)
pub_user_message(payload, connection_id, "Systembericht wurde versandt.", MessageType.SUCCESS)
if json_rsp is not None:
if json_rsp.get("error"):
pub_user_message(payload, connection_id,
f"Fehler: {json_rsp.get('message')}",
MessageType.ERROR)
elif json_rsp.get("status") == "created":
pub_user_message(payload, connection_id,
f"Neues Ticket {json_rsp.get('ticket_id')} erstellt.",
MessageType.SUCCESS)
elif json_rsp.get("status") == "updated":
pub_user_message(payload, connection_id,
f"Systembericht bestehendem Ticket {json_rsp.get('ticket_id')} hinzugefügt.",
MessageType.SUCCESS)
else:
pub_user_message(payload, connection_id,
"Fehler beim Erstellen des Systemberichts.",
MessageType.ERROR)

def getChargeLog(self, connection_id: str, payload: dict) -> None:
Pub().pub(f'openWB/set/log/{connection_id}/data', get_log_data(payload["data"]))
Expand Down
21 changes: 12 additions & 9 deletions packages/helpermodules/create_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ def get_boots(num_lines=100):
return ''.join(lines[-num_lines:])


def create_debug_log(input_data):
def create_debug_log(input_data) -> Optional[dict]:
def write_to_file(file_handler, func, default: Optional[Any] = None):
try:
file_handler.write(func()+"\n")
Expand Down Expand Up @@ -432,6 +432,7 @@ def write_to_file(file_handler, func, default: Optional[Any] = None):
write_to_file(df, lambda: f"# section: uuids #\n{get_uuids()}\n")
write_to_file(df, lambda: f"# section: boots #\n{get_boots(30)}\n")
write_to_file(df, lambda: f'# section: storage #\n{run_command(["df", "-h"])}\n')
write_to_file(df, lambda: 'Extended_Debug_Section\n')
write_to_file(df, lambda: f"# section: broker essentials #\n{broker.get_broker_essentials()}\n")
write_to_file(
df, lambda: f"# section: retained log #\n{merge_log_files('main', 500)}")
Expand All @@ -455,20 +456,22 @@ def write_to_file(file_handler, func, default: Optional[Any] = None):
log.info("***** uploading debug log...")
with open(debug_file, 'rb') as f:
data = f.read()
req.get_http_session().put("https://openwb.de/tools/debug3.php",
data=data,
params={
'debugemail': debug_email,
'ticketnumber': ticketnumber,
'subject': subject
},
timeout=10)
json_rsp = req.get_http_session().put("https://debughandler.wb-solution.de",
data=data,
params={
'debugemail': debug_email,
'ticketnumber': ticketnumber,
'subject': subject
},
timeout=10).json()

log.info("***** cleanup...")
os.remove(debug_file)
log.info("***** debug log end")
return json_rsp
except Exception as e:
log.exception(f"Error creating debug log: {e}")
return None


class BrokerContent:
Expand Down
27 changes: 26 additions & 1 deletion packages/helpermodules/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,25 +109,50 @@ def filter_pos(name: str, record) -> bool:


class InMemoryLogHandler(logging.Handler):
def __init__(self, base_handler=None):
def __init__(self, base_handler=None, max_size_mb=50):
super().__init__()
self.base_handler = base_handler
self.log_stream = io.StringIO()
self.has_warning_or_error = False
self.max_size_bytes = max_size_mb * 1024 * 1024 # Convert MB to bytes
self.line_count = 0

def emit(self, record):
if self.base_handler is None or self.base_handler.filter(record):
msg = self.format(record)
self.log_stream.write(msg + '\n')
self.line_count += 1

# Check size every 100 lines to avoid performance overhead
if self.line_count % 100 == 0:
current_size = len(self.log_stream.getvalue().encode('utf-8'))
if current_size > self.max_size_bytes:
self._truncate_logs()

if record.levelno >= logging.WARNING:
self.has_warning_or_error = True

def _truncate_logs(self):
"""Keep only the last 25% of logs when size limit is exceeded"""
current_logs = self.log_stream.getvalue()
lines = current_logs.split('\n')

# Keep only the last 25% of lines
keep_count = max(100, len(lines) // 4) # At least 100 lines
kept_lines = lines[-keep_count:]

# Reset the stream with truncated content
self.log_stream = io.StringIO()
self.log_stream.write('\n'.join(kept_lines))
self.line_count = len(kept_lines)

def get_logs(self):
return self.log_stream.getvalue()

def clear(self):
self.log_stream = io.StringIO()
self.has_warning_or_error = False
self.line_count = 0


def clear_in_memory_log_handler(logger_name: str = None) -> None:
Expand Down
3 changes: 2 additions & 1 deletion packages/helpermodules/setdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,8 @@ def process_bat_topic(self, msg: mqtt.MQTTMessage):
if ("openWB/set/bat/config/bat_control_permitted" in msg.topic or
"openWB/set/bat/config/configured" in msg.topic or
"openWB/set/bat/get/power_limit_controllable" in msg.topic or
"openWB/set/bat/set/regulate_up" in msg.topic):
"openWB/set/bat/set/regulate_up" in msg.topic or
"openWB/set/bat/set/hysteresis_discharge" in msg.topic):
self._validate_value(msg, bool)
elif "openWB/set/bat/set/charging_power_left" in msg.topic:
self._validate_value(msg, float)
Expand Down
2 changes: 1 addition & 1 deletion packages/helpermodules/subdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,7 @@ def process_system_topic(self, client: mqtt.Client, var: dict, msg: mqtt.MQTTMes
log.debug("skipping mqtt bridge message on startup")
elif "mqtt" and "valid_partner_ids" in msg.topic:
# duplicate topic for remote support service
log.error(f"received valid partner ids: {decode_payload(msg.payload)}")
log.debug(f"received valid partner ids: {decode_payload(msg.payload)}")
Pub().pub("openWB-remote/valid_partner_ids", decode_payload(msg.payload))
# will be moved to separate handler!
elif "GetRemoteSupport" in msg.topic:
Expand Down
6 changes: 3 additions & 3 deletions packages/modules/common/evse.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def activate_precise_current(self) -> None:
else:
with ModifyLoglevelContext(log, logging.DEBUG):
log.debug("Bit zur Angabe der Ströme in 0,1A-Schritten wird gesetzt.")
self.client.write_registers(2005, value ^ self.PRECISE_CURRENT_BIT, unit=self.id)
self.client.write_register(2005, value ^ self.PRECISE_CURRENT_BIT, unit=self.id)
# Zeit zum Verarbeiten geben
time.sleep(1)

Expand All @@ -105,7 +105,7 @@ def deactivate_precise_current(self) -> None:
if value & self.PRECISE_CURRENT_BIT:
with ModifyLoglevelContext(log, logging.DEBUG):
log.debug("Bit zur Angabe der Ströme in 0,1A-Schritten wird zurueckgesetzt.")
self.client.write_registers(2005, value ^ self.PRECISE_CURRENT_BIT, unit=self.id)
self.client.write_register(2005, value ^ self.PRECISE_CURRENT_BIT, unit=self.id)
else:
return

Expand All @@ -118,4 +118,4 @@ def set_current(self, current: int, phases_in_use: Optional[int] = None) -> None
if formatted_current > 16 and phases_in_use > 1:
formatted_current = 16
if self.evse_current != formatted_current:
self.client.write_registers(1000, formatted_current, unit=self.id)
self.client.write_register(1000, formatted_current, unit=self.id)
Loading