EV-03 round 2: a missing car power is unknown, and mode3 E/F has a state
- CarTracker.car_w returns None when the current reading carries no power (power entity unavailable or in a non-power unit); it can no longer raise into the control cycle, /status or the MQTT values. - mode3 E and F are the card state "fault" (EN and NL). - chargersim gains fault(). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -495,8 +495,8 @@ def census_entity(opts: dict) -> str:
|
||||
# --------------------------------------------------------------------------- #
|
||||
# The car's state, kept over time
|
||||
# --------------------------------------------------------------------------- #
|
||||
CHARGING, WAITING, NONE_, FULL, UNREACH, SETUP = (
|
||||
"charging", "waiting", "none", "full", "unreach", "setup")
|
||||
CHARGING, WAITING, NONE_, FULL, UNREACH, SETUP, FAULT = (
|
||||
"charging", "waiting", "none", "full", "unreach", "setup", "fault")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -559,11 +559,18 @@ class CarTracker:
|
||||
return self.last_ok is not None and now - self.last_ok <= self.stale_s
|
||||
|
||||
def car_w(self, now: float):
|
||||
"""The car's power now, or None when the reading is not fresh."""
|
||||
if not self.fresh(now) or self.reading is None or \
|
||||
not self.reading.available:
|
||||
"""The car's power now, or None (unknown) when the reading is not fresh
|
||||
OR the current reading carries no power. Never the last good figure: a
|
||||
read that says "no power" contradicts it, and absence is never zero. It
|
||||
never raises (telemetry has no foot in the control path)."""
|
||||
r = self.reading
|
||||
if not self.fresh(now) or r is None or not r.available \
|
||||
or r.power_w is None:
|
||||
return None
|
||||
try:
|
||||
return max(0.0, float(r.power_w))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return max(0.0, float(self.reading.power_w))
|
||||
|
||||
# -- the two-writer detector ----------------------------------------------
|
||||
def _writer(self, r, now_utc):
|
||||
@@ -634,6 +641,8 @@ class CarTracker:
|
||||
r = self.reading
|
||||
if r is None or not r.available:
|
||||
return None if self.last_good is None else self.last_status
|
||||
if r.state in ("E", "F"): # the charger's own error states
|
||||
return FAULT
|
||||
if r.state == "A" or (r.state is None and r.connected is False):
|
||||
return NONE_
|
||||
if r.state in ("B", "C", "D") or r.connected:
|
||||
|
||||
@@ -1226,7 +1226,7 @@ def _docs_url():
|
||||
# EV-03: the Car card. READ ONLY: Off is the only mode in this version.
|
||||
# --------------------------------------------------------------------------- #
|
||||
_CAR_KIND = {"charging": "ok", "full": "ok", "unreach": "warn",
|
||||
"setup": "warn"}
|
||||
"setup": "warn", "fault": "warn"}
|
||||
|
||||
|
||||
def car_state_word(key) -> str:
|
||||
|
||||
@@ -301,8 +301,8 @@
|
||||
"warning": "See Needs attention below."
|
||||
},
|
||||
"car": {
|
||||
"state": {"charging": "Charging", "waiting": "Connected, waiting", "none": "No car connected", "full": "Car is full", "unreach": "Charger not reachable", "setup": "Charger not set up", "wait": "No reading yet"},
|
||||
"big": {"waiting": "Connected, not charging.", "none": "Nothing to charge.", "full": "The car is full.", "seen": "Last seen at {t}.", "no_reading": "No reading.", "setup": "Not set up.", "wait": "No reading yet."},
|
||||
"state": {"charging": "Charging", "waiting": "Connected, waiting", "none": "No car connected", "full": "Car is full", "unreach": "Charger not reachable", "setup": "Charger not set up", "fault": "Charger fault", "wait": "No reading yet"},
|
||||
"big": {"waiting": "Connected, not charging.", "none": "Nothing to charge.", "full": "The car is full.", "seen": "Last seen at {t}.", "no_reading": "No reading.", "setup": "Not set up.", "fault": "The charger reports a fault.", "wait": "No reading yet."},
|
||||
"phase_one": "1 phase",
|
||||
"phase_many": "{n} phases",
|
||||
"today": "{kwh} charged today.",
|
||||
@@ -316,6 +316,7 @@
|
||||
"not_found": "The add-on cannot find the charger. Check the current setting entity in Settings.",
|
||||
"limit": "The charger limit is above what the charger allows. Lower it in Settings.",
|
||||
"name": "The current setting entity does not fit the charger. Check it in Settings.",
|
||||
"fault": "The charger reports a fault. Check it at the charger itself.",
|
||||
"wait": "Waiting for the first reading."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -255,8 +255,8 @@
|
||||
"pill": {"running": "Actief", "shadow": "Schaduwmodus", "stopped": "Gestopt", "alarm": "Heeft u nu nodig", "waiting": "Wacht op Home Assistant", "warning": "Actief, met een waarschuwing"},
|
||||
"banner_sub": {"running": "De batterijen volgen het plan hieronder.", "shadow": "De plannen hieronder tonen wat de add-on zou doen.", "stopped": "Start de regeling hieronder als u klaar bent.", "alarm": "Zie Vraagt aandacht hieronder voor wat u moet doen.", "waiting": "De regeling gaat vanzelf verder zodra Home Assistant antwoordt.", "warning": "Zie Vraagt aandacht hieronder."},
|
||||
"car": {
|
||||
"state": {"charging": "Laadt", "waiting": "Aangesloten, wacht", "none": "Geen auto aangesloten", "full": "Auto is vol", "unreach": "Laadpaal onbereikbaar", "setup": "Laadpaal niet ingesteld", "wait": "Nog geen meting"},
|
||||
"big": {"waiting": "Aangesloten, laadt niet.", "none": "Niets te laden.", "full": "De auto is vol.", "seen": "Laatst gezien om {t}.", "no_reading": "Geen meting.", "setup": "Niet ingesteld.", "wait": "Nog geen meting."},
|
||||
"state": {"charging": "Laadt", "waiting": "Aangesloten, wacht", "none": "Geen auto aangesloten", "full": "Auto is vol", "unreach": "Laadpaal onbereikbaar", "setup": "Laadpaal niet ingesteld", "fault": "Laadpaal storing", "wait": "Nog geen meting"},
|
||||
"big": {"waiting": "Aangesloten, laadt niet.", "none": "Niets te laden.", "full": "De auto is vol.", "seen": "Laatst gezien om {t}.", "no_reading": "Geen meting.", "setup": "Niet ingesteld.", "fault": "De laadpaal meldt een storing.", "wait": "Nog geen meting."},
|
||||
"phase_one": "1 fase",
|
||||
"phase_many": "{n} fasen",
|
||||
"today": "Vandaag {kwh} geladen.",
|
||||
@@ -270,6 +270,7 @@
|
||||
"not_found": "De add-on vindt de laadpaal niet. Controleer de entiteit van de laadstroom bij Instellingen.",
|
||||
"limit": "De laadpaalgrens is hoger dan de laadpaal toestaat. Verlaag ze bij Instellingen.",
|
||||
"name": "De entiteit van de laadstroom past niet bij de laadpaal. Controleer ze bij Instellingen.",
|
||||
"fault": "De laadpaal meldt een storing. Controleer ze aan de laadpaal zelf.",
|
||||
"wait": "Wacht op de eerste meting."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -944,6 +944,102 @@ check("an absent number goes out as HA's own 'None' payload, and the PV rule "
|
||||
"(ABSENT_AS_UNKNOWN is one key) is not widened",
|
||||
mqtt.ABSENT_AS_UNKNOWN == frozenset({"pv"}))
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("\n10. ROUND 2: A MISSING POWER READING IS AN UNKNOWN TERM, NEVER AN ERROR")
|
||||
# --------------------------------------------------------------------------- #
|
||||
import traceback # noqa: E402
|
||||
|
||||
|
||||
def bad_power(kind):
|
||||
"""A controller with one good reading, then (inside stale_s) an AVAILABLE
|
||||
reading whose power is missing: `unavailable`, or in a non-power unit."""
|
||||
sim_ = SimCharger()
|
||||
sim_.plug_in()
|
||||
sim_.set_phases(3)
|
||||
sim_.set_current(10)
|
||||
site_ = Site(sim_, grid=7500.0, batt=0.0, pv=500.0)
|
||||
opts = dict(CAR, pv_forecast_entity="sensor.fc", battery_modes_enabled=True)
|
||||
c_ = build(opts, site_)
|
||||
del samples[:]
|
||||
c_.house_profile = _HP()
|
||||
asyncio.run(run_cycles(c_, site_, n=8, sim=sim_))
|
||||
good_w = c_.car.reading.power_w
|
||||
pw = f"sensor.{sim_.x}_active_power_total_socket_1"
|
||||
orig = site_._all
|
||||
|
||||
def broken():
|
||||
out = orig()
|
||||
if kind == "unavailable":
|
||||
out[pw] = dict(out[pw], state="unavailable")
|
||||
else:
|
||||
out[pw] = dict(out[pw], attributes={"unit_of_measurement": "V"})
|
||||
return out
|
||||
site_._all = broken
|
||||
del samples[:]
|
||||
res = {"good_w": good_w, "err": None}
|
||||
try:
|
||||
CLOCK[0] = 1000.0 + 8
|
||||
asyncio.run(rin(c_, 1008.0)) # 1 s after the good one
|
||||
res["fresh"] = c_.car.fresh(CLOCK[0])
|
||||
res["avail"] = c_.car.reading.available and c_.car.reading.power_w is None
|
||||
res["term"] = c_.car_term()
|
||||
res["now"] = c_.status_payload()["now"]
|
||||
res["card"] = c_.status_payload()["car"]
|
||||
asyncio.run(c_.cycle()) # the trade cycle's profile step
|
||||
res["sample"] = samples[-1] if samples else "none"
|
||||
res["vals"] = c_.car_values()
|
||||
except Exception: # noqa: BLE001
|
||||
res["err"] = traceback.format_exc().strip().splitlines()[-1]
|
||||
return res
|
||||
|
||||
|
||||
for kind in ("unavailable", "wrong unit"):
|
||||
r_ = bad_power(kind)
|
||||
check(f"{kind}: the reading is available with no power, and still inside "
|
||||
f"stale_s (the case that raised)", r_["err"] is None
|
||||
and r_.get("fresh") is True and r_.get("avail") is True)
|
||||
check(f"{kind}: car_term, /status, the trade cycle and the MQTT values do "
|
||||
f"not raise ({r_['err']})", r_["err"] is None)
|
||||
check(f"{kind}: the term is UNKNOWN (never 0 W): House shows no reading and "
|
||||
f"the profile sample is None", r_["err"] is None
|
||||
and r_["term"] == (0.0, True) and r_["now"]["house_w"] is None
|
||||
and r_["sample"] is None)
|
||||
check(f"{kind}: the card shows no power figure and the MQTT power is the "
|
||||
f"None payload", r_["err"] is None
|
||||
and r_["card"]["power_w"] is None
|
||||
and r_["vals"]["car_power"] == mqtt.PAYLOAD_NONE)
|
||||
|
||||
# E and F: the charger's own error states
|
||||
for letter in ("E", "F"):
|
||||
rr = CH.ChargerReading(available=True, state=letter, power_w=0.0,
|
||||
allowed_a=10.0, setpoint_a=10.0)
|
||||
tk = CH.CarTracker()
|
||||
tk.observe(rr, 1.0, now_utc(), D)
|
||||
check(f"mode3 {letter} is the defined state 'fault', not a blank",
|
||||
tk.status(1.0) == "fault")
|
||||
sim_f = SimCharger()
|
||||
sim_f.plug_in()
|
||||
sim_f.set_current(10)
|
||||
site_f = Site(sim_f)
|
||||
c_f = build(CAR, site_f)
|
||||
asyncio.run(run_cycles(c_f, site_f, n=4, sim=sim_f))
|
||||
sim_f.fault("E")
|
||||
sim_f.tick(1)
|
||||
asyncio.run(rin(c_f, 1010.0))
|
||||
pf = c_f.status_payload()
|
||||
check("fault card (EN): chip, big line, Why, amber",
|
||||
pf["car"]["chip"] == "Charger fault" and pf["car"]["kind"] == "warn"
|
||||
and pf["car"]["big"] == "The charger reports a fault."
|
||||
and pf["car"]["why"] == "The charger reports a fault. Check it at the "
|
||||
"charger itself.")
|
||||
pfn = c_f.status_payload(lang="nl")
|
||||
check("fault card (NL)", pfn["car"]["chip"] == "Laadpaal storing"
|
||||
and pfn["car"]["big"] == "De laadpaal meldt een storing."
|
||||
and pfn["car"]["why"] == "De laadpaal meldt een storing. Controleer ze "
|
||||
"aan de laadpaal zelf.")
|
||||
check("the MQTT state says it too", c_f.car_values()["car_state"]
|
||||
== "Charger fault")
|
||||
|
||||
print()
|
||||
if fails:
|
||||
print(f"{len(fails)} FAILED")
|
||||
|
||||
@@ -72,6 +72,10 @@ class SimCharger:
|
||||
def set_phases(self, n):
|
||||
self.phases_allowed = 3 if n == 3 else 1
|
||||
|
||||
def fault(self, letter="E"):
|
||||
"""mode3 E or F: the charger's own error states."""
|
||||
self.state, self.power_w = letter, 0.0
|
||||
|
||||
def go_unavailable(self, on=True):
|
||||
self.unavailable = bool(on)
|
||||
|
||||
@@ -90,6 +94,9 @@ class SimCharger:
|
||||
|
||||
def tick(self, seconds=1.0):
|
||||
for _ in range(int(max(1, round(seconds)))):
|
||||
if self.state in ("E", "F"): # a fault: nothing flows, it stays
|
||||
self.power_w = 0.0
|
||||
continue
|
||||
if self.state != "A":
|
||||
self.state = "C" if self.target_w() > 0 else "B"
|
||||
tgt = self.target_w()
|
||||
@@ -125,7 +132,7 @@ class SimCharger:
|
||||
st(f"sensor.{x}_mode3_state_socket_1", f"STATE_{self.state}"),
|
||||
st(f"sensor.{x}_display_state_socket_1",
|
||||
{"A": "Available", "B": "EV Connected",
|
||||
"C": "Charging Normal"}[self.state]),
|
||||
"C": "Charging Normal"}.get(self.state, "Error")),
|
||||
st(f"sensor.{x}_active_power_total_socket_1",
|
||||
f"{self.power_w:.0f}", "W"),
|
||||
st(f"sensor.{x}_meter_reading_socket_1", f"{self.kwh:.2f}",
|
||||
|
||||
Reference in New Issue
Block a user