diff --git a/goodwe_controller/CHANGELOG.md b/goodwe_controller/CHANGELOG.md index 63bf561..0af8a9f 100644 --- a/goodwe_controller/CHANGELOG.md +++ b/goodwe_controller/CHANGELOG.md @@ -2,7 +2,7 @@ ### Not in a release yet -- Each inverter is now reached through a driver, so another make can be added later. Nothing changes for a GoodWe site. The inverter list also accepts five new optional settings that nothing reads yet, and the schema no longer requires a setpoint entity per entry: a GoodWe entry without one is still refused at start ([[SMA-2]]). +- Each inverter is now reached through a driver, so another make can be added later. Nothing changes for a GoodWe site ([[SMA-2]]). - Home now draws today's charge from the grid for every hour the add-on will really charge, not only the run under way ([[UX-77]]). - When Home Assistant passes readings on late, control no longer swings on them: it waits until the battery shows its last power request, and the peak limit no longer asks for full power on them. The page says so, and if the readings stay too late every inverter goes to 0 W ([[SAFETY-128]]). - With a 5 s meter, readings 30 s late are now noticed within 100 seconds, not 3 minutes ([[SAFETY-129]]). diff --git a/goodwe_controller/DECISIONS/SMA-2-driver-seam.md b/goodwe_controller/DECISIONS/SMA-2-driver-seam.md index f904173..a6777c0 100644 --- a/goodwe_controller/DECISIONS/SMA-2-driver-seam.md +++ b/goodwe_controller/DECISIONS/SMA-2-driver-seam.md @@ -30,19 +30,26 @@ one unit named twice is the SAFETY-28 refusal. The census still counts every with 23ff9d0 on 349 option shapes is a test). The driver binds its Home Assistant on first use by the Controller, so a unit added later is wired too. -**Schema.** The brief says "`setpoint_entity` becomes optional" (SMA-2, "Schema per -inverter entry"; SMA-9 section 1.1): it is the GoodWe driver's own key, so a -driver reached by host and port has none. `setpoint_entity` is optional per entry -in config.yaml; rule 9(d) still refuses a GoodWe entry without it, with the same -sentence (`test_sma2_driver.py` part 6), beside `driver` (only `goodwe_esphome`), -`host`, `port`, `unit_id`, `maintenance`. The last four are checked for shape -and not read. The Supervisor shape probe was re-run (section K). +**Schema, and why the new keys are not in it.** The brief asks for +`driver`, `host`, `port`, `unit_id`, `maintenance` per entry and for +`setpoint_entity` to become optional. Owner decision 2026-11-07: clients see +nothing new until SMA-4/SMA-7 use the keys. The Supervisor form shows every +schema key (and marks required ones), and a nested entry with a key the schema +lacks has that key silently dropped (measured, shape probe section K), so the +only way to hide them is to keep them out of config.yaml. So for now config.yaml, the +translations and the labels are exactly 23ff9d0's, and `setpoint_entity` stays +required there: until a driver without one is selectable, no entry can lack it. +The code underneath is the brief's: the driver defaults to `goodwe_esphome`, +rule 9(d) is per driver (a GoodWe entry without a setpoint entity is refused, +same sentence), and rule 9(h) checks `driver`, `host`, `port`, `unit_id`, +`maintenance` when a caller that skips the schema (the panel's save path, +sim/inject, the checks) supplies them; those sentences name the key, having no +label. When SMA-4 needs them: add them to the schema as optional with +translations, make `setpoint_entity` `str?`, and add them to `uilayout` as card +fields or a hidden list. **Checked against 23ff9d0:** replay digests and a log of every Home Assistant call are byte-identical at N=1 and N=2; the TEL-07 announce diff is 0/0/0; a fake driver beside a GoodWe unit allocates, grades, drops and publishes per unit; a call site that goes round the seam is caught. -**Per-entry keys the page does not draw** are listed in `uilayout.ROSTER_HIDDEN`; -test_uilayout and test_sma2_driver fail for a key that is neither a card field -nor hidden, and a save through the page keeps them (a test). diff --git a/goodwe_controller/app/messages.py b/goodwe_controller/app/messages.py index d6d1bbe..831da93 100644 --- a/goodwe_controller/app/messages.py +++ b/goodwe_controller/app/messages.py @@ -1683,11 +1683,6 @@ LABELS = { "capacity_kwh": "Usable battery capacity", "inverters": "Inverter list", "name": "Name", - "driver": "Inverter type", - "host": "Inverter address", - "port": "Inverter port", - "unit_id": "Inverter unit number", - "maintenance": "Battery maintenance", "meter_source": "Where the meter reading comes from", "meter_phases": "Number of phases", "p1_host": "HomeWizard P1 address", @@ -1769,11 +1764,6 @@ LABELS_NL = { "capacity_kwh": "Bruikbare capaciteit", "inverters": "Lijst met omvormers", "name": "Naam", - "driver": "Type omvormer", - "host": "Adres van de omvormer", - "port": "Poort van de omvormer", - "unit_id": "Eenheidsnummer van de omvormer", - "maintenance": "Onderhoud van de batterij", "meter_source": "Waar de meterstand vandaan komt", "meter_phases": "Aantal fasen", "p1_host": "Adres van de HomeWizard P1", diff --git a/goodwe_controller/app/uilayout.py b/goodwe_controller/app/uilayout.py index d19bf9f..421dac7 100644 --- a/goodwe_controller/app/uilayout.py +++ b/goodwe_controller/app/uilayout.py @@ -302,19 +302,6 @@ DERIVED = { HIDDEN: dict = {"price_service_token": "a secret, set on the Configuration tab"} -# SMA-2. Per-entry keys of the inverter list the page never shows. They pick and -# address an inverter driver, only `goodwe_esphome` is selectable, and nothing -# reads the other four yet. An entry's keys the page does not draw travel -# through a save as stored (uimodel._roster_decode keeps the whole entry). -ROSTER_HIDDEN: dict = { - "driver": "only goodwe_esphome for now; empty means that", - "host": "not read yet", - "port": "not read yet", - "unit_id": "not read yet", - "maintenance": "not read yet", -} - - def fields(): """Every field once, in page order, roster fields after `inverters`.""" for group in GROUPS: diff --git a/goodwe_controller/app/validate.py b/goodwe_controller/app/validate.py index 9d9dec4..1993187 100644 --- a/goodwe_controller/app/validate.py +++ b/goodwe_controller/app/validate.py @@ -814,6 +814,12 @@ def _whole(raw, lo: int, hi: int): def _driver_problems(entry: dict, who: str) -> list[str]: """Rule 9(h) (SMA-2): the keys that choose and address an inverter driver. + NOT IN config.yaml's SCHEMA, so the Supervisor form and its validator never + see them (owner decision 2026-11-07: clients see nothing new until SMA-4). + They can still reach this rule from callers that skip the schema (the + panel's save path, sim/inject, the checks), so they are graded here, and + named by key because they have no label. + Only `goodwe_esphome` is selectable for now. `host`, `port`, `unit_id` and `maintenance` are accepted and checked for shape, and nothing reads them yet: a later driver's entry must not be refused for carrying them. @@ -822,21 +828,21 @@ def _driver_problems(entry: dict, who: str) -> list[str]: raw = entry.get("driver") if raw not in (None, "") and str(raw).strip() not in drivers.SELECTABLE: out.append( - f"{who} has {S('driver')} {messages.shown(raw)}. Use " + f"{who} has `inverters.driver` {messages.shown(raw)}. Use " f"{', '.join(drivers.SELECTABLE)}, or leave it empty.") host = entry.get("host") if host not in (None, "") and (not isinstance(host, str) or re.search(r"\s", host.strip())): - out.append(f"{who} has {S('host')} {messages.shown(host)}, which is " + out.append(f"{who} has `inverters.host` {messages.shown(host)}, which is " "not an address. Use a name or an IP address.") for key, lo, hi in (("port", 1, 65535), ("unit_id", 0, 247)): val = entry.get(key) if val not in (None, "") and _whole(val, lo, hi) is None: - out.append(f"{who} has {S(key)} {messages.shown(val)}. Use a " + out.append(f"{who} has `inverters.{key}` {messages.shown(val)}. Use a " f"whole number from {lo} to {hi}.") maint = entry.get("maintenance") if maint is not None and not isinstance(maint, bool): - out.append(f"{who} has {S('maintenance')} {messages.shown(maint)}. " + out.append(f"{who} has `inverters.maintenance` {messages.shown(maint)}. " "Use on or off.") return out diff --git a/goodwe_controller/config.yaml b/goodwe_controller/config.yaml index 1754991..b02e072 100644 --- a/goodwe_controller/config.yaml +++ b/goodwe_controller/config.yaml @@ -942,17 +942,11 @@ schema: # batteries, whereas an absent capacity has an honest answer - no opinion # about the split, every unit weighs 1.0, which is what shipped. # - # SMA-2. `driver` empty means goodwe_esphome, the only choice for now. - # `host`, `port`, `unit_id` and `maintenance` are accepted and checked for - # shape; nothing reads them yet (DECISIONS/SMA-2-driver-seam.md). - # - # ⚠️ THREE KEYS ARE REQUIRED PER ENTRY AND THAT IS DELIBERATE. An entry with + # ⚠️ FOUR KEYS ARE REQUIRED PER ENTRY AND THAT IS DELIBERATE. An entry with # no `name` has no Store namespace and would share another unit's SAFETY-02 - # latch; one with no `max_w` would have to borrow a rating, which is the - # sibling-grading defect DEV-02 removed; `bms_soc_floor` is SAFETY-32's - # (options block above). `setpoint_entity` WAS A FOURTH; since SMA-2 it is the - # GoodWe driver's own key, so a unit nothing can command is refused by - # validate.py rule 9(d) per driver rather than by this schema. + # latch; one with no `setpoint_entity` is a unit nothing can command; one with + # no `max_w` would have to borrow a rating, which is the sibling-grading + # defect DEV-02 removed; `bms_soc_floor` is SAFETY-32's (options block above). # The optional keys are the sensors, whose absence honestly means "this unit # reports none", and `capacity_kwh`. # @@ -964,18 +958,13 @@ schema: # test_inverter.py part 12 asserts the ordering is still hostile. inverters: - name: str - setpoint_entity: str? + setpoint_entity: str max_w: int(100,5000) bms_soc_floor: int(0,50) soc_entity: str? batt_entity: str? batt_invert: bool? capacity_kwh: float(0.1,200)? - driver: list(goodwe_esphome)? - host: str? - port: port? - unit_id: int(0,247)? - maintenance: bool? estop_fitted: bool battery_modes_enabled: bool diff --git a/goodwe_controller/test_inverter.py b/goodwe_controller/test_inverter.py index 1fe17cb..4b7281e 100644 --- a/goodwe_controller/test_inverter.py +++ b/goodwe_controller/test_inverter.py @@ -1536,9 +1536,8 @@ _decl9 = dict(ln.strip().lstrip("- ").split(": ", 1) for ln in _entry9.splitlines()[1:] if ": " in ln) print(f" the shipped per-entry schema: {_decl9}") check("...and the schema half declares it as a LIST OF OBJECTS whose `name`, " - "`max_w` and `bms_soc_floor` are REQUIRED - no " - "trailing `?` (SMA-2: `setpoint_entity` is the GoodWe driver's own key " - "and optional here; rule 9(d) requires it per driver) - so the Supervisor refuses a half-written entry before the " + "`setpoint_entity`, `max_w` and `bms_soc_floor` are REQUIRED - no " + "trailing `?` - so the Supervisor refuses a half-written entry before the " "add-on sees it, and an entry can never fall back on a sibling's rating " "or on a sibling's BMS cut-out because there is no entry without one. " "⚠️ SAFETY-32 ADDED THE FOURTH ONE AS REQUIRED RATHER THAN AS " @@ -1554,14 +1553,13 @@ check("...and the schema half declares it as a LIST OF OBJECTS whose `name`, " "refuses every options blob written before it existed", set(_decl9) == {"name", "setpoint_entity", "max_w", "bms_soc_floor", "soc_entity", "batt_entity", "batt_invert", - "capacity_kwh", "driver", "host", "port", "unit_id", - "maintenance"} + "capacity_kwh"} and not any(_decl9[k].endswith("?") - for k in ("name", "max_w", "bms_soc_floor")) + for k in ("name", "setpoint_entity", "max_w", + "bms_soc_floor")) and all(_decl9[k].endswith("?") - for k in ("setpoint_entity", "soc_entity", "batt_entity", - "batt_invert", "capacity_kwh", "driver", "host", - "port", "unit_id", "maintenance"))) + for k in ("soc_entity", "batt_entity", "batt_invert", + "capacity_kwh"))) # ⚠️ NOTHING IS INHERITED, AND THIS IS DEV-02 RESTATED AS A CONFIG RULE. # ⚠️ THE FLAT KEYS ARE SET TO REAL ENTITIES HERE, AND THAT IS THE WHOLE CHECK. diff --git a/goodwe_controller/test_sma2_driver.py b/goodwe_controller/test_sma2_driver.py index e98b296..42b57fa 100644 --- a/goodwe_controller/test_sma2_driver.py +++ b/goodwe_controller/test_sma2_driver.py @@ -592,22 +592,29 @@ ent = sch[sch.index("\n inverters:"):] ent = ent[:ent.index("\n\n")] decl = dict(ln.strip().lstrip("- ").split(": ", 1) for ln in ent.splitlines()[1:] if ": " in ln) -check("setpoint_entity is optional per entry, beside driver, host, port, " - "unit_id and maintenance; name, max_w and bms_soc_floor stay required", - decl["setpoint_entity"] == "str?" and decl["driver"] == "list(goodwe_esphome)?" - and decl["host"] == "str?" and decl["port"] == "port?" - and decl["unit_id"] == "int(0,247)?" and decl["maintenance"] == "bool?" - and not any(decl[k].endswith("?") for k in ("name", "max_w", - "bms_soc_floor"))) -check("config.yaml lists exactly the selectable drivers", - decl["driver"] == f"list({'|'.join(SELECTABLE)})?") +NEW_KEYS = ("driver", "host", "port", "unit_id", "maintenance") +check("HIDDEN FROM CLIENTS (owner, 2026-11-07): config.yaml's per-entry schema " + "is what it was on 23ff9d0 - the five driver keys are not in it, and " + "setpoint_entity is still required - so the Supervisor form shows " + "nothing new", + not set(NEW_KEYS) & set(decl) and not decl["setpoint_entity"].endswith("?") + and set(decl) == {"name", "setpoint_entity", "max_w", "bms_soc_floor", + "soc_entity", "batt_entity", "batt_invert", + "capacity_kwh"}) +_tr = {lg: io.open(HERE / "translations" / f"{lg}.yaml", encoding="utf-8").read() + for lg in ("en", "nl")} +from app import messages as _msgs # noqa: E402 +check("...and no translation, label or description was added for them", + not any(f" {k}:\n" in t.split(" estop_fitted:")[0].split(" inverters:")[1] + for t in _tr.values() for k in NEW_KEYS) + and not set(NEW_KEYS) & (set(_msgs.LABELS) | set(_msgs.LABELS_NL))) # ============================================================================== print() print("6. round 2: census parity, setters, rule 9(d), NaN ports, hidden keys") import copy, itertools, math # noqa: E402 -from app import uilayout as L, uimodel # noqa: E402 +from app import uimodel # noqa: E402 from app.validate import ACTUATOR_WRITE_KEYS # noqa: E402 @@ -708,25 +715,13 @@ for key in ("port", "unit_id"): got, ok_ = repr(e), False check(f"{key} = {nm} is refused by key and does not raise", ok_) -keys_cfg = set(decl) -placed = {k.split(".", 1)[1] for _g, _p, f in L.fields() - for k in [f.key] if k.startswith("inverters.")} -check("ROSTER_HIDDEN: every per-entry key in config.yaml is either a card " - "field or hidden, never both and never neither", - all((k in placed) != (k in L.ROSTER_HIDDEN) for k in keys_cfg) - and not (placed & set(L.ROSTER_HIDDEN)) - and set(L.ROSTER_HIDDEN) <= keys_cfg) -check("...and a per-entry key that is neither would be flagged (test_uilayout " - "grades the same rule with a made-up key)", - not all((k in placed) != (k in L.ROSTER_HIDDEN) - for k in keys_cfg | {"brand_new_key"})) ent = dict(name="a", setpoint_entity="number.a", max_w=3000, bms_soc_floor=10, soc_entity="sensor.s", batt_entity="sensor.b", driver="goodwe_esphome", host="h", port=502, unit_id=3, maintenance=False) opts_rt = {"inverters": [ent, dict(ent, name="b", setpoint_entity="number.b")]} back = uimodel.ui_to_options(uimodel.options_to_ui(copy.deepcopy(opts_rt))) -check("the page model carries the hidden keys through a save untouched " - "(this is how ROSTER_HIDDEN keys survive the settings page)", +check("an entry that carries the driver keys (from a caller that skips the " + "schema) comes back from a save through the page model unchanged", back.get("inverters") == opts_rt["inverters"]) del REGISTRY["fake"] diff --git a/goodwe_controller/test_translations.py b/goodwe_controller/test_translations.py index a05f169..47e1db8 100644 --- a/goodwe_controller/test_translations.py +++ b/goodwe_controller/test_translations.py @@ -75,7 +75,7 @@ check(f"the option surface was actually read - {len(ORDER)} options, not zero", check("...and the repeatable `inverters:` section's per-entry keys were read " "too, which is the half a top-level-only reader would miss", NESTED.get("inverters", [])[:2] == ["name", "setpoint_entity"] - and len(NESTED["inverters"]) == 13) # SMA-2: +5 driver keys + and len(NESTED["inverters"]) == 8) print("\n2. AC: every option and every per-entry key is translated, in BOTH " "languages") @@ -281,7 +281,7 @@ check("...and the accepted subset really does round-trip the shipped files - " f"per-entry keys came back out of each, which is what says the " f"strictness above is not just refusing everything", all(len(TR[lg]["configuration"]) == len(ORDER) - and len(TR[lg]["configuration"]["inverters"]["fields"]) == 13 + and len(TR[lg]["configuration"]["inverters"]["fields"]) == 8 for lg in LANGS)) print(" ⚠️ the licence for this hand parser is NOT this file. " "tools/supervisor_shape_probe.py section F loads the same two files with " diff --git a/goodwe_controller/test_uilayout.py b/goodwe_controller/test_uilayout.py index ab68084..70c4bdb 100644 --- a/goodwe_controller/test_uilayout.py +++ b/goodwe_controller/test_uilayout.py @@ -51,10 +51,8 @@ def coverage_problems(order, nested): out += [f"{k} is not in config.yaml" for k in set(placed) | set(L.DERIVED) | set(L.HIDDEN) if k not in order] sub = [k.split(".", 1)[1] for k in KEYS if k.startswith("inverters.")] - # SMA-2: a per-entry key may instead be listed in L.ROSTER_HIDDEN (the - # driver keys the page does not draw), and then it must not also be a field. out += [f"inverters.{k} has no card field" for k in nested.get("inverters", []) - if sub.count(k) + (k in L.ROSTER_HIDDEN) != 1] + if sub.count(k) != 1] out += [f"inverters.{k} is not in config.yaml" for k in sub if k not in nested.get("inverters", [])] return out diff --git a/goodwe_controller/tools/supervisor_shape_probe.py b/goodwe_controller/tools/supervisor_shape_probe.py index c370844..6e1b1f4 100644 --- a/goodwe_controller/tools/supervisor_shape_probe.py +++ b/goodwe_controller/tools/supervisor_shape_probe.py @@ -243,6 +243,8 @@ show("the per-entry BASELINE is ACCEPTED, so every refusal below has nowhere " "else to come from", ok, json.dumps(out.get("inverters")) if ok else out) for title, entry, expect in ( ("no name", {k: v for k, v in D_FULL.items() if k != "name"}, "name"), + ("no setpoint_entity", + {k: v for k, v in D_FULL.items() if k != "setpoint_entity"}, "setpoint_entity"), ("no max_w", {k: v for k, v in D_FULL.items() if k != "max_w"}, "max_w"), ("no bms_soc_floor (SAFETY-32)", {k: v for k, v in D_FULL.items() if k != "bms_soc_floor"}, "bms_soc_floor"), @@ -255,39 +257,22 @@ for title, entry, expect in ( ok, out = validate(dict(D_BASE, inverters="[]")) show("a JSON STRING where the list belongs is refused", not ok, out) -print("\n== K. SMA-2: the driver keys on an inverter entry ==") -# `setpoint_entity` is OPTIONAL per entry since SMA-2 (it is the GoodWe driver's -# own key; validate.py rule 9(d) requires it for GoodWe), beside `driver`, -# `host`, `port`, `unit_id` and `maintenance`. Same method as §D: an ACCEPTED -# baseline, ONE value varied, the refusal asserted on the KEY'S NAME. -K_FULL = dict(D_FULL, driver="goodwe_esphome", host="inverter.local", port=502, - unit_id=1, maintenance=False) -ok, out = validate(dict(D_BASE, inverters=[K_FULL])) -show("an entry carrying every new key is ACCEPTED, and the keys survive", - ok and all(out["inverters"][0].get(k) == K_FULL[k] - for k in ("driver", "host", "port", "unit_id", "maintenance")), - json.dumps(out.get("inverters")) if ok else out) +print("\n== K. SMA-2: the driver keys are NOT in the schema (owner, 2026-11-07) ==") +# `driver`, `host`, `port`, `unit_id` and `maintenance` are graded by validate.py +# rule 9(h) but kept out of config.yaml so the Configuration tab shows nothing +# new. This pins both halves: the shipped entry is the one it always was, and the +# Supervisor (measured) silently DROPS an unknown key from an entry, so they +# cannot arrive by the form or by YAML. ok, out = validate(dict(D_BASE, inverters=[D_FULL])) -show("an entry from before SMA-2 (none of the new keys) is ACCEPTED unchanged", - ok and out["inverters"] == [D_FULL], out) -ok, out = validate(dict(D_BASE, inverters=[ - {k: v for k, v in D_FULL.items() if k != "setpoint_entity"}])) -show("an entry with NO setpoint_entity is now accepted by the schema " - "(rule 9(d) refuses it for GoodWe, at startup)", ok, out) -# ⚠️ THE SUPERVISOR'S REFUSAL NAMES THE VALUE'S RULE, NOT THE KEY, FOR A NESTED -# ENTRY (§D's "at most 5000"), so the expectation is that rule's own words. -for title, entry, expect in ( - ("driver other than goodwe_esphome", dict(K_FULL, driver="sma"), - "one of ['goodwe_esphome']"), - ("port 0", dict(K_FULL, port=0), "at least 1"), - ("port 70000", dict(K_FULL, port=70000), "at most 65535"), - ("unit_id 248", dict(K_FULL, unit_id=248), "at most 247"), - ("host a number", dict(K_FULL, host=5), "expected str"), - ("maintenance a word that is not a boolean", - dict(K_FULL, maintenance="banana"), "boolean")): - ok, out = validate(dict(D_BASE, inverters=[entry])) - show(f"an entry with {title} is refused, with the rule's own words", - (not ok) and expect in str(out), out) +show("the pre-SMA-2 entry is ACCEPTED unchanged", ok and out["inverters"] == [D_FULL], out) +for _k in ("driver", "host", "port", "unit_id", "maintenance"): + ok, out = validate(dict(D_BASE, inverters=[dict(D_FULL, **{_k: "x"})])) + show(f"an entry carrying `{_k}` is accepted and the key is dropped", + ok and out["inverters"] == [D_FULL], out) +show("the per-entry schema has exactly the eight keys it had", + sorted(schema["inverters"][0]) == sorted(["name", "setpoint_entity", "max_w", + "bms_soc_floor", "soc_entity", "batt_entity", "batt_invert", "capacity_kwh"]), + json.dumps(schema["inverters"])) print("\n== E. the ROLLBACK direction, and the operator's UI ==") diff --git a/goodwe_controller/translations/en.yaml b/goodwe_controller/translations/en.yaml index a23daba..d390d74 100644 --- a/goodwe_controller/translations/en.yaml +++ b/goodwe_controller/translations/en.yaml @@ -79,21 +79,6 @@ configuration: capacity_kwh: name: "Usable battery capacity (kWh) (optional)" description: "This battery's usable capacity from its datasheet. Fill it in on every inverter or on none. The add-on then shares the work by the energy each battery has left." - driver: - name: "Inverter type (optional)" - description: "The kind of inverter. Leave it empty for a GoodWe." - host: - name: "Inverter address (optional)" - description: "Not used yet. Leave it empty." - port: - name: "Inverter port (optional)" - description: "Not used yet. Leave it empty." - unit_id: - name: "Inverter unit number (optional)" - description: "Not used yet. Leave it empty." - maintenance: - name: "Battery maintenance (optional)" - description: "Not used yet. Leave it empty." estop_fitted: name: "Hardware · RS485 emergency stop is fitted" description: "Switch this on if the emergency stop is installed. It only changes a warning on the status page." diff --git a/goodwe_controller/translations/nl.yaml b/goodwe_controller/translations/nl.yaml index 0eecb3b..3db9bd5 100644 --- a/goodwe_controller/translations/nl.yaml +++ b/goodwe_controller/translations/nl.yaml @@ -60,21 +60,6 @@ configuration: capacity_kwh: name: "Bruikbare capaciteit (kWh) (optioneel)" description: "De bruikbare capaciteit van deze batterij, van het datablad. Vul ze bij elke omvormer in of bij geen. Dan verdeelt de add-on het werk volgens de energie in elke batterij." - driver: - name: "Type omvormer (optioneel)" - description: "Het soort omvormer. Laat leeg voor een GoodWe." - host: - name: "Adres van de omvormer (optioneel)" - description: "Nog niet in gebruik. Laat leeg." - port: - name: "Poort van de omvormer (optioneel)" - description: "Nog niet in gebruik. Laat leeg." - unit_id: - name: "Eenheidsnummer van de omvormer (optioneel)" - description: "Nog niet in gebruik. Laat leeg." - maintenance: - name: "Onderhoud van de batterij (optioneel)" - description: "Nog niet in gebruik. Laat leeg." estop_fitted: name: "Hardware · RS485-noodstop is geplaatst" description: "Zet dit aan als de noodstop geïnstalleerd is. Het verandert alleen een waarschuwing op de statuspagina."