EV-03 round 3: the power entity is required and reads in W or kW
A charger with no usable power entity is refused as "not set up", like a missing setpoint, read-back or state entity, so House and the house profile behave as with no charger. An entity that is merely unavailable at probe time is still the unreachable path. NL: help text "met rust", chip "Laadpaalstoring". Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -79,11 +79,15 @@ ROLES = {
|
||||
"error": ("sensor", "display_error_number_socket_1"),
|
||||
"login": ("binary_sensor", "https_api_login_status"),
|
||||
}
|
||||
#: a missing one of these refuses the charger (not the add-on)
|
||||
REQUIRED = ("current", "readback", "state")
|
||||
#: a missing one of these refuses the charger (not the add-on). `power` is one
|
||||
#: of them: without it the car's power is unknown for good, House would show no
|
||||
#: reading for ever and the house profile would drop every sample.
|
||||
#: The power entity must also read in W or kW (`POWER_UNITS`).
|
||||
REQUIRED = ("current", "readback", "state", "power")
|
||||
_SETPOINT_RE = re.compile(r"^number\.(?P<x>.+)_" + SETPOINT_SUFFIX + "$")
|
||||
|
||||
_ROLE_ORDER = tuple(ROLES)
|
||||
POWER_UNITS = ("W", "kW")
|
||||
|
||||
|
||||
def census_key(entity) -> str:
|
||||
@@ -151,7 +155,7 @@ def _amps(obj):
|
||||
return None
|
||||
|
||||
|
||||
_W_PER = {"mW": 0.001, "W": 1.0, "kW": 1000.0, "MW": 1000000.0}
|
||||
_W_PER = {"W": 1.0, "kW": 1000.0}
|
||||
|
||||
|
||||
def _watts(obj):
|
||||
@@ -258,6 +262,9 @@ class ChargerProbe:
|
||||
missing: tuple = ()
|
||||
station_a: float | None = None
|
||||
derived: bool = True # False: the setpoint's name does not fit
|
||||
power_unit: str | None = None # the power entity's unit, when it is
|
||||
# reading a number and that unit is not W or kW (an unavailable entity
|
||||
# has no unit to judge: that is the unreachable path, not a refusal)
|
||||
|
||||
@property
|
||||
def required_missing(self) -> tuple:
|
||||
@@ -265,7 +272,8 @@ class ChargerProbe:
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return self.derived and not self.required_missing
|
||||
return (self.derived and not self.required_missing
|
||||
and self.power_unit is None)
|
||||
|
||||
|
||||
def probe_from(setpoint, exists) -> ChargerProbe:
|
||||
@@ -278,9 +286,18 @@ def probe_from(setpoint, exists) -> ChargerProbe:
|
||||
(found if exists(ents[role]) is not None else missing).append(
|
||||
(role, ents[role]))
|
||||
st = {r: exists(e) for r, e in ents.items()}
|
||||
raw, unit, _ = _state_obj(st.get("power"))
|
||||
bad_unit = None
|
||||
if raw is not None and unit not in POWER_UNITS:
|
||||
try:
|
||||
float(raw)
|
||||
bad_unit = unit or "none"
|
||||
except ValueError:
|
||||
pass
|
||||
return ChargerProbe(str(setpoint), tuple(found), tuple(missing),
|
||||
station_a=_first(_amps(st.get("station_active")),
|
||||
_amps(st.get("station"))))
|
||||
_amps(st.get("station"))),
|
||||
power_unit=bad_unit)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@@ -157,7 +157,13 @@ class InputsMixin:
|
||||
whose limit is above the station limit, is REFUSED: the add-on starts
|
||||
and the Car card says so. Said once."""
|
||||
why = None
|
||||
if not pr.ok:
|
||||
if not pr.ok and pr.power_unit is not None \
|
||||
and not pr.required_missing:
|
||||
why = "unit"
|
||||
_LOG.warning("The car charger is not set up, because its power "
|
||||
"does not read in W or kW: CHARGER_POWER_UNIT.")
|
||||
_LOG.debug("The charger power entity reads in %r.", pr.power_unit)
|
||||
elif not pr.ok:
|
||||
why = "not_found"
|
||||
_LOG.warning("The car charger is not set up, because Home "
|
||||
"Assistant has no entity it needs: CHARGER_NOT_FOUND.")
|
||||
|
||||
@@ -1268,7 +1268,8 @@ def _car_why(key, c, good):
|
||||
return _t("car.why.unreach_a", a=f"{a:.0f}", brand=brand)
|
||||
return _t("car.why.unreach", brand=brand)
|
||||
if key == "setup":
|
||||
why = {"not_found": "not_found", "limit": "limit"}.get(
|
||||
why = {"not_found": "not_found", "limit": "limit",
|
||||
"unit": "unit"}.get(
|
||||
c.charger_refused or "", "name")
|
||||
return _t("car.why." + why)
|
||||
return _t("car.why." + (key if key else "wait"))
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="small muted" id="car-today"></div>
|
||||
<div class="small car-lab" data-t="car.mode"></div>
|
||||
<div class="seg" role="radiogroup" id="car-mode" data-t-label="car.mode"><button type="button" role="radio" aria-checked="true" data-t="car.mode_off"></button></div>
|
||||
<p class="tiny muted" style="margin:6px 0 0" data-t="car.help_off"></p>
|
||||
<p class="tiny muted" style="margin:6px 0 0" data-t="car.help_off.description"></p>
|
||||
<div class="car-why small"><span class="muted" data-t="car.why"></span><span id="car-why"></span></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"loading": "Loading the forecast."
|
||||
},
|
||||
"peak": {"name": "Peak limit", "this_month": "This month's peak", "limit": "Limit", "none": "None recorded yet", "unreadable": "Cannot be read now"},
|
||||
"car": {"name": "Car", "mode": "Mode", "mode_off": "Off", "help_off": "The add-on leaves the charger alone.", "why": "Why"},
|
||||
"car": {"name": "Car", "mode": "Mode", "mode_off": "Off", "help_off": {"description": "The add-on leaves the charger alone."}, "why": "Why"},
|
||||
"attention": {"name": "Needs attention", "nothing": "Nothing to look at", "to_look": "to look at", "details": "Details", "alarm": "Alarm", "warning": "Warning", "info": "Info"},
|
||||
"control": {"name": "Control", "stop": "Stop control", "start": "Start control", "maint_start": "Start a battery cycle now", "maint_abort": "Stop the battery cycle", "docs": "Add-on documentation", "more": "More actions"},
|
||||
"details": {"name": "Details for installers", "rows": "Status rows", "checks": "Checks", "event": "Last event"},
|
||||
@@ -314,6 +314,7 @@
|
||||
"unreach_a": "The charger keeps its last setting, {a} A. Check the {brand} integration in Home Assistant.",
|
||||
"unreach": "The add-on cannot read the charger. Check the {brand} integration in Home Assistant.",
|
||||
"not_found": "The add-on cannot find the charger. Check the current setting entity in Settings.",
|
||||
"unit": "The charger power does not read in W or kW. Check the charger 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.",
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"loading": "De prognose wordt geladen."
|
||||
},
|
||||
"peak": {"name": "Piekgrens", "this_month": "Piek van deze maand", "limit": "Grens", "none": "Nog niets gemeten", "unreadable": "Nu niet leesbaar"},
|
||||
"car": {"name": "Auto", "mode": "Modus", "mode_off": "Uit", "help_off": "De add-on laat de laadpaal ongemoeid.", "why": "Waarom"},
|
||||
"car": {"name": "Auto", "mode": "Modus", "mode_off": "Uit", "help_off": {"description": "De add-on laat de laadpaal met rust."}, "why": "Waarom"},
|
||||
"attention": {"name": "Vraagt aandacht", "nothing": "Niets om na te kijken", "to_look": "na te kijken", "details": "Details", "alarm": "Alarm", "warning": "Waarschuwing", "info": "Info"},
|
||||
"control": {"name": "Regeling", "stop": "Regeling stoppen", "start": "Regeling starten", "maint_start": "Nu een batterijcyclus starten", "maint_abort": "Batterijcyclus stoppen", "docs": "Documentatie van de add-on", "more": "Meer acties"},
|
||||
"details": {"name": "Details voor installateurs", "rows": "Statusregels", "checks": "Controles", "event": "Laatste gebeurtenis"},
|
||||
@@ -255,7 +255,7 @@
|
||||
"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", "fault": "Laadpaal storing", "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": "Laadpaalstoring", "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",
|
||||
@@ -268,6 +268,7 @@
|
||||
"unreach_a": "De laadpaal houdt haar laatste instelling, {a} A. Controleer de {brand}-integratie in Home Assistant.",
|
||||
"unreach": "De add-on kan de laadpaal niet lezen. Controleer de {brand}-integratie in Home Assistant.",
|
||||
"not_found": "De add-on vindt de laadpaal niet. Controleer de entiteit van de laadstroom bij Instellingen.",
|
||||
"unit": "Het vermogen van de laadpaal is niet in W of kW. Controleer de laadpaal 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.",
|
||||
|
||||
@@ -1991,6 +1991,7 @@ def charger_probe_rows(opts: dict, entities: dict | None) -> dict:
|
||||
pr = chargers.probe_from(setpoint, state_obj)
|
||||
return {"ok": pr.ok, "found": [list(x) for x in pr.found],
|
||||
"missing": [list(x) for x in pr.missing], "limit_a": pr.station_a,
|
||||
"power_unit": pr.power_unit,
|
||||
"state": "ok" if pr.ok else "missing"}
|
||||
|
||||
|
||||
@@ -2032,11 +2033,26 @@ def _charger_notes(notes, opts: dict, entities: dict) -> None:
|
||||
return
|
||||
for role, entity in look["missing"]:
|
||||
required = role in chargers.REQUIRED
|
||||
_advise(notes, "warn" if required else "info", key,
|
||||
f"Home Assistant has no entity {entity}, which the charger "
|
||||
f"needs.",
|
||||
nl=f"Home Assistant kent {entity} niet, die de laadpaal "
|
||||
"nodig heeft.")
|
||||
# A missing required entity (current, read-back, car state or power)
|
||||
# means the charger is not used: the Car card says "not set up".
|
||||
if required:
|
||||
_advise(notes, "warn", key,
|
||||
f"Home Assistant has no entity {entity}, which the "
|
||||
"charger needs, so it is not used.",
|
||||
nl=f"Home Assistant kent {entity} niet, die de laadpaal "
|
||||
"nodig heeft, dus ze wordt niet gebruikt.")
|
||||
else:
|
||||
_advise(notes, "info", key,
|
||||
f"Home Assistant has no entity {entity}, which the "
|
||||
"charger can also use.",
|
||||
nl=f"Home Assistant kent {entity} niet, die de laadpaal "
|
||||
"ook kan gebruiken.")
|
||||
if look.get("power_unit") is not None:
|
||||
_advise(notes, "warn", key,
|
||||
"The charger's power does not read in W or kW, so the "
|
||||
"charger is not used.",
|
||||
nl="Het vermogen van de laadpaal is niet in W of kW, dus ze "
|
||||
"wordt niet gebruikt.")
|
||||
lim = look["limit_a"]
|
||||
if look["ok"]:
|
||||
have = {r for r, _e in look["found"]}
|
||||
|
||||
@@ -981,7 +981,7 @@ def bad_power(kind):
|
||||
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["avail"] = c_.car.reading.power_w is None
|
||||
res["term"] = c_.car_term()
|
||||
res["now"] = c_.status_payload()["now"]
|
||||
res["card"] = c_.status_payload()["car"]
|
||||
@@ -995,8 +995,8 @@ def bad_power(kind):
|
||||
|
||||
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
|
||||
check(f"{kind}: the reading carries no power and is still inside stale_s "
|
||||
f"(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)
|
||||
@@ -1033,13 +1033,117 @@ check("fault card (EN): chip, big line, Why, amber",
|
||||
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"
|
||||
check("fault card (NL)", pfn["car"]["chip"] == "Laadpaalstoring"
|
||||
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("\n11. ROUND 3: THE POWER ENTITY IS REQUIRED, AND READS IN W OR kW")
|
||||
# --------------------------------------------------------------------------- #
|
||||
PW = "sensor.wallbox_active_power_total_socket_1"
|
||||
rows_all = {eid: {"entity_id": eid, "state": s["state"],
|
||||
"unit": s["attributes"].get("unit_of_measurement")}
|
||||
for eid, s in SimCharger().states().items()}
|
||||
no_power = {k: v for k, v in rows_all.items() if k != PW}
|
||||
look = validate.charger_probe_for(SET, no_power)
|
||||
check("the probe refuses a charger with no power entity",
|
||||
not look["ok"] and ["power", PW] in look["missing"])
|
||||
look = validate.charger_probe_for(SET, dict(rows_all, **{PW: dict(
|
||||
rows_all[PW], unit="V")}))
|
||||
check("...and one whose power reads in a wrong unit (V)",
|
||||
not look["ok"] and look["power_unit"] == "V")
|
||||
check("...and one with no unit at all",
|
||||
validate.charger_probe_for(SET, dict(rows_all, **{PW: dict(
|
||||
rows_all[PW], unit=None)}))["power_unit"] == "none")
|
||||
check("kW is accepted (converted), W too",
|
||||
validate.charger_probe_for(SET, dict(rows_all, **{PW: dict(
|
||||
rows_all[PW], unit="kW")}))["ok"]
|
||||
and validate.charger_probe_for(SET, rows_all)["ok"])
|
||||
look = validate.charger_probe_for(SET, dict(rows_all, **{PW: dict(
|
||||
rows_all[PW], state="unavailable", unit=None)}))
|
||||
check("a power entity that is UNAVAILABLE at probe time is not a refusal (it "
|
||||
"is the unreachable path)", look["ok"] and not look["missing"])
|
||||
nts = validate.config_notes(CAR, entities=no_power)
|
||||
check("the settings note for the missing power entity says the charger is not "
|
||||
"used (a warning, not info)",
|
||||
any(n.tier == "warn" and PW in n.text and "not used" in n.text
|
||||
for n in nts))
|
||||
nts = validate.config_notes(CAR, entities=dict(rows_all, **{PW: dict(
|
||||
rows_all[PW], unit="V")}))
|
||||
check("...and so does a wrong unit",
|
||||
any(n.tier == "warn" and "W or kW" in n.text for n in nts))
|
||||
|
||||
|
||||
class Hide(Site):
|
||||
"""The site, with some of the charger's entities hidden or changed."""
|
||||
|
||||
def __init__(self, charger, hide=(), unit=None, **kw):
|
||||
super().__init__(charger, **kw)
|
||||
self.hide, self.unit = set(hide), unit
|
||||
|
||||
def _all(self):
|
||||
out = {k: v for k, v in super()._all().items() if k not in self.hide}
|
||||
if self.unit and PW in out:
|
||||
out[PW] = dict(out[PW], attributes={"unit_of_measurement":
|
||||
self.unit})
|
||||
return out
|
||||
|
||||
|
||||
def world(site_factory, opts, n=10):
|
||||
"""(controller, the Home now block, the last profile sample)."""
|
||||
del samples[:]
|
||||
sim_ = SimCharger()
|
||||
sim_.plug_in()
|
||||
sim_.set_phases(3)
|
||||
sim_.set_current(10)
|
||||
site_ = site_factory(sim_)
|
||||
c_ = build(dict(opts, pv_forecast_entity="sensor.fc",
|
||||
battery_modes_enabled=True), site_)
|
||||
c_.house_profile = _HP()
|
||||
asyncio.run(run_cycles(c_, site_, n=n, sim=sim_))
|
||||
return c_, c_.status_payload(), samples[-1] if samples else "none"
|
||||
|
||||
|
||||
c_none, p_none, s_none = world(lambda s_: Site(s_, grid=7500.0, pv=500.0),
|
||||
BASE)
|
||||
for why, factory, want in (
|
||||
("no power entity", lambda s_: Hide(s_, hide={PW}, grid=7500.0,
|
||||
pv=500.0), "not_found"),
|
||||
("a power entity in V", lambda s_: Hide(s_, unit="V", grid=7500.0,
|
||||
pv=500.0), "unit")):
|
||||
c_x, p_x, s_x = world(factory, CAR)
|
||||
check(f"{why}: the charger is refused as 'not set up' ({c_x.charger_refused})",
|
||||
c_x.charger_refused == want and not c_x.charger.configured()
|
||||
and p_x["car"]["state"] == "setup"
|
||||
and p_x["car"]["chip"] == "Charger not set up")
|
||||
check(f"{why}: House and the house profile are as with no charger at all "
|
||||
f"(House {p_x['now']['house_w']}, sample {s_x})",
|
||||
p_x["now"] == p_none["now"] and s_x == s_none
|
||||
and p_x["now"]["house_w"] is not None and s_x is not None)
|
||||
check(f"{why}: the add-on still runs (the inverter is written) and one "
|
||||
f"amber row says so", any(e == "number.sp" for e, _v in
|
||||
c_x.hass.sets)
|
||||
and [x["id"] for x in c_x.checks() if x["id"].startswith("charger")]
|
||||
== ["charger_setup"])
|
||||
check("the unit refusal's Why is plain and in both languages",
|
||||
"does not read in W or kW" in p_x["car"]["why"]
|
||||
and "niet in W of kW" in c_x.status_payload(lang="nl")["car"]["why"])
|
||||
# unavailable at start: NOT refused, the unreachable path
|
||||
c_u, p_u, _s = world(lambda s_: Hide(s_, grid=7500.0, pv=500.0), CAR, n=3)
|
||||
sim_u = SimCharger()
|
||||
sim_u.plug_in()
|
||||
sim_u.go_unavailable()
|
||||
site_u = Site(sim_u)
|
||||
c_u2 = build(CAR, site_u)
|
||||
asyncio.run(run_cycles(c_u2, site_u, n=40, sim=sim_u))
|
||||
check("a charger that is unavailable from the first read is not refused; it "
|
||||
"is the unreachable path", c_u2.charger_refused is None
|
||||
and c_u2.charger.configured()
|
||||
and c_u2.status_payload()["car"]["state"] == "unreach")
|
||||
|
||||
print()
|
||||
if fails:
|
||||
print(f"{len(fails)} FAILED")
|
||||
|
||||
Reference in New Issue
Block a user