Run 1: 2nd-write-wins 9/10, 1st-write-wins 1/10
Run 2: 2nd-write-wins 7/10, 1st-write-wins 3/10
20/20 POSTs return HTTP 200. In 1-3 of 10 iterations the earlier request's value persists instead of the later one.
Any client that emits parallel writes to this endpoint can observe lost updates.
#!/usr/bin/env python3
import json
import os
import sys
import time
import urllib.request
import urllib.error
from concurrent.futures import ThreadPoolExecutor
from http.cookiejar import CookieJar
from collections import Counter
EVCC_HOST = os.environ.get("EVCC_HOST", "https://evcc:port")
VEHICLE_NAME = os.environ.get("VEHICLE_NAME", "db:18")
EVCC_PASSWORD = os.environ.get("EVCC_PASSWORD", "")
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(CookieJar()))
def http(method, url, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(
url, data=data, method=method,
headers={"Content-Type": "application/json"} if data else {},
)
try:
with opener.open(req, timeout=10) as r:
return r.status, r.read().decode(errors="replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode(errors="replace")
except Exception as e:
return None, f"ERR: {type(e).__name__}: {e}"
def get_state():
s, b = http("GET", f"{EVCC_HOST}/api/state")
if s != 200:
print(f" /api/state -> HTTP {s}: {b[:200]}")
return None
try:
d = json.loads(b)
except Exception as e:
print(f" JSON parse error: {e}")
return None
return d.get("result", d)
def precondition_from(state):
if not state:
return None
for lp in state.get("loadpoints", []):
if lp.get("vehicleName") == VEHICLE_NAME:
return lp.get("effectivePlanStrategy", {}).get("precondition")
lps = state.get("loadpoints", [])
return lps[0].get("effectivePlanStrategy", {}).get("precondition") if lps else None
def login():
if not EVCC_PASSWORD:
return True
s, b = http("POST", f"{EVCC_HOST}/api/auth/login", {"password": EVCC_PASSWORD})
print(f"Login -> HTTP {s} ({b[:120]})")
return s == 200
def main():
print(f"EVCC_HOST = {EVCC_HOST}")
print(f"VEHICLE_NAME = {VEHICLE_NAME}")
print(f"PASSWORD set = {bool(EVCC_PASSWORD)}")
print()
print("=== Sanity ===")
state = get_state()
if not state:
sys.exit(1)
print(f"Top-level keys ({len(state)}): {sorted(state.keys())[:10]} ...")
print(f"Vehicle '{VEHICLE_NAME}' present: {VEHICLE_NAME in state.get('vehicles', {})}")
lps = state.get("loadpoints", [])
print(f"Loadpoint count: {len(lps)}")
for i, lp in enumerate(lps):
print(f" LP{i+1}: vehicleName={lp.get('vehicleName')!r}, "
f"effectivePlanStrategy={lp.get('effectivePlanStrategy')}")
print()
if EVCC_PASSWORD:
print("=== Login ===")
if not login():
print("Login failed. Abort.")
sys.exit(1)
print()
print("=== C: Single write probe ===")
url = f"{EVCC_HOST}/api/vehicles/{VEHICLE_NAME}/plan/strategy"
print(f"URL: {url}")
s, b = http("POST", url, {"continuous": False, "precondition": 900})
print(f"POST -> HTTP {s}, body[:200]={b[:200]!r}")
if s not in (200, 201, 204):
print("Write probe failed. Check auth / endpoint. Abort.")
sys.exit(1)
print()
print("=== Race test (10 iterations) ===")
results = []
with ThreadPoolExecutor(max_workers=2) as ex:
for i in range(1, 11):
http("POST", url, {"continuous": False, "precondition": 900})
time.sleep(0.1)
f1 = ex.submit(http, "POST", url, {"continuous": False, "precondition": 1800})
f2 = ex.submit(http, "POST", url, {"continuous": False, "precondition": 3600})
s1, _ = f1.result()
s2, _ = f2.result()
time.sleep(1.0)
final = precondition_from(get_state())
results.append(final)
print(f"Run {i:2d}: w1=HTTP{s1}, w2=HTTP{s2}, final={final}")
print()
print("=== Summary ===")
for val, count in Counter(results).most_common():
print(f" {val!s:>10}: {count}/10")
if __name__ == "__main__":
main()
Describe the bug
Concurrent POSTs to
/api/vehicles/{id}/plan/strategyare all accepted (HTTP 200) but committed in non-deterministic order — the earlier request can become the final persisted state.Reproduction: a Python script fires two parallel POSTs with distinct
preconditionvalues and reads backeffectivePlanStrategy.preconditionfrom/api/state. 10 iterations, two runs against the same evcc instance:20/20 POSTs return HTTP 200. In 1-3 of 10 iterations the earlier request's value persists instead of the later one.
Any client that emits parallel writes to this endpoint can observe lost updates.
Steps to reproduce
Configuration details
.Log details
What type of operating system or environment does evcc run on?
Linux
External automation
Nightly build
Version
v0.306.3