|
|
|
@@ -1,35 +1,185 @@
|
|
|
|
|
# Changelog
|
|
|
|
|
|
|
|
|
|
## Unreleased
|
|
|
|
|
## 0.10.0
|
|
|
|
|
|
|
|
|
|
⚠️ **THIS HAS NO VERSION AND CANNOT REACH AN INSTALL UNTIL IT GETS ONE.**
|
|
|
|
|
PKG-06's and PKG-07's rule: the Supervisor keys the built image by `version:`,
|
|
|
|
|
so `ha apps update` is a silent no-op on an unbumped change. This branch
|
|
|
|
|
deliberately does **not** pick the number, because the right one depends on a
|
|
|
|
|
fact the branch cannot observe - whether 0.9.0's image has already been built
|
|
|
|
|
and pulled. If it has not, this belongs inside 0.9.0's section before it ships;
|
|
|
|
|
if it has, it needs 0.10.0. Whoever deploys decides.
|
|
|
|
|
⚠️ **A CONFIG THAT IS LEGAL ON 0.9.0 IS REFUSED ON 0.10.0, AND THAT - NOT THE
|
|
|
|
|
NUMBER OF TICKETS - IS WHY THIS IS A MINOR BUMP RATHER THAN A PATCH.** Same
|
|
|
|
|
shape as 0.8.0's `slew_w` rule and the same test: is there a stored options
|
|
|
|
|
blob the previous version started on and this one will not? There is. A refusal
|
|
|
|
|
is `SystemExit(78)` (`EX_CONFIG`) - the add-on does not start, ⚠️ **and the
|
|
|
|
|
ingress panel goes with it**, because the add-on serving it is not running, so
|
|
|
|
|
recovery means the Supervisor UI.
|
|
|
|
|
|
|
|
|
|
Measured on the reference install's own exported blob (70 keys), merged over
|
|
|
|
|
this version's 75 defaults the way the Supervisor merges it, inside the CI
|
|
|
|
|
image. The three below are what a reader who checks only one thing should
|
|
|
|
|
check.
|
|
|
|
|
|
|
|
|
|
* ⚠️ **[[TARIFF-07]] - THREE `price_provider` IDS ARE RETIRED, AND A SITE SET
|
|
|
|
|
TO ONE OF THEM IS REFUSED BY NAME AT STARTUP.** `engie_dynamic`,
|
|
|
|
|
`totalenergies_dynamic` and `eneco_eddy` are gone from `PROVIDERS`. Each was
|
|
|
|
|
accepted, silently, at every startup on 0.9.0 and earlier; on 0.10.0 each is
|
|
|
|
|
a refusal that names the id, the endpoint, what that endpoint actually
|
|
|
|
|
answers - a 404 from a gateway with no route, a TLS handshake against a
|
|
|
|
|
hostname the certificate does not cover, a 404 with an empty body - which
|
|
|
|
|
tariff the site has really been running since the id was entered, and what to
|
|
|
|
|
set instead. **The refusal IS the migration.** Every fetch any of the three
|
|
|
|
|
ever made ended in `SourceUnavailable`, and §11.4 turns that into a SILENT
|
|
|
|
|
fall back to the static profile, so there is no working behaviour to preserve
|
|
|
|
|
and nothing on the status page ever differed from a provider that simply
|
|
|
|
|
never found a cheap hour. ⚠️ **All three ids stay in `config.yaml`'s
|
|
|
|
|
`price_provider` enum on purpose**: drop one and the Supervisor rejects the
|
|
|
|
|
options file *before the add-on runs*, the operator gets an opaque `expected
|
|
|
|
|
one of ...`, and `validate.py`'s message is never seen - on exactly the sites
|
|
|
|
|
that need it. `PROVIDERS` is three; the enum is seven.
|
|
|
|
|
⚠️ **The reference install is on `price_provider: off` and is unaffected.**
|
|
|
|
|
|
|
|
|
|
* **[[TARIFF-18]] - `price_taxes_eur_kwh` moves from `0.06` to `0.11736`, and
|
|
|
|
|
the non-obvious half is the refusal that deliberately does NOT happen.** A
|
|
|
|
|
site that *stored* `0.06` is unaffected in both directions. It keeps `0.06`,
|
|
|
|
|
because `App.options` is `deepmerge(config.yaml defaults, stored options)`
|
|
|
|
|
and a moved default reaches only keys the blob does not carry - so the new
|
|
|
|
|
figure arrives at a **fresh** install, and at an existing one only when its
|
|
|
|
|
options are re-saved. ⚠️ **AND IT DOES NOT BECOME A STARTUP REFUSAL EITHER,
|
|
|
|
|
WHICH TOOK A MECHANISM.** [[TARIFF-17]] made a `price_taxes_profile` period
|
|
|
|
|
table beside a **non-default** `price_taxes_eur_kwh` a refusal, comparing
|
|
|
|
|
against the shipped default precisely because the Supervisor writes the key
|
|
|
|
|
whether the operator typed it or not. Moving the default turns every stored
|
|
|
|
|
`0.06` into "non-default" **with nobody editing anything**, so a working
|
|
|
|
|
Walloon or Brussels site with a period table would have stopped starting on
|
|
|
|
|
this upgrade, over an inert number the table overrides anyway.
|
|
|
|
|
`price.py` carries `SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,)` and
|
|
|
|
|
`validate.py` reads a superseded default as *untouched*. **The rung is not
|
|
|
|
|
weakened**: the same table beside a figure that is neither the shipped nor a
|
|
|
|
|
superseded default - ORES's `0.1198` - is still refused, by name, and the
|
|
|
|
|
refusal now says which two figures it will accept.
|
|
|
|
|
|
|
|
|
|
* **[[SAFETY-10]] - a new `shadow_mode` option, shipped `auto`, and BOTH halves
|
|
|
|
|
of what that does matter.** ⚠️ **AN EXISTING INSTALL THAT IS CONTROLLING DOES
|
|
|
|
|
NOT CHANGE BY ONE WATT.** `auto` is not a constant the Supervisor merges; it
|
|
|
|
|
is a value the add-on **resolves**, and it resolves to `not control_started`
|
|
|
|
|
- `shadow.engaged()` reads `Controller.auto`, the persisted start/stop state.
|
|
|
|
|
An install with `auto_start: true`, or whose operator has pressed *Start /
|
|
|
|
|
stop control*, is started, so `auto` resolves to live. An install that is
|
|
|
|
|
**stopped** keeps its actuator at 0 W exactly as before, from the same writer
|
|
|
|
|
on the same heartbeat; what changes is what the status page says and that the
|
|
|
|
|
decision path now runs and is recorded. ⚠️ **A FRESH INSTALL COMES UP
|
|
|
|
|
OBSERVING**: it ships `auto_start: false`, has no stored `auto` key, and
|
|
|
|
|
therefore runs its whole decision path in shadow before it is ever allowed to
|
|
|
|
|
command anything. That is the "default-on" the card asked for, delivered
|
|
|
|
|
where it is safe and withheld where it is not - there is no mechanism in the
|
|
|
|
|
add-on options system by which one key can default differently for a new
|
|
|
|
|
install and an existing one, so a literal `shadow_mode: true` default would
|
|
|
|
|
have stopped the battery at the reference site on the next update.
|
|
|
|
|
**Measured cross-revision** - the same stimulus through the shipped
|
|
|
|
|
`Controller.cycle()` on `8f82dd4` (the merge before SAFETY-10) and on
|
|
|
|
|
`e6da478` (SAFETY-10 itself), start button ON: the write streams are
|
|
|
|
|
**byte-identical**, 13 / 13 / 25 writes on a 6000 W import, a 4000 W export
|
|
|
|
|
and a 300 W trickle, same values in the same order. An install that is
|
|
|
|
|
**stopped** is byte-identical across the same pair too, at one 0 W write.
|
|
|
|
|
⚠️ The positive control is what makes that readable: the same driver with
|
|
|
|
|
`shadow_mode: on` and the button ON gets 13 writes up to 5000 W on the older
|
|
|
|
|
tree and **one 0 W write** on the newer, so the driver does reach shadow mode
|
|
|
|
|
and an identical pair is a result rather than a silence. The shadow case is
|
|
|
|
|
also graded in `test_clamp.py` part 8 on what reached `set_number()`, against
|
|
|
|
|
a precondition run that puts 30 non-zero writes on the wire. ⚠️ An
|
|
|
|
|
unrecognised `shadow_mode` fails toward inaction
|
|
|
|
|
**and** is refused by name (rule 11), so the reinterpretation is never
|
|
|
|
|
silent; no blob written before 0.10.0 can carry one.
|
|
|
|
|
|
|
|
|
|
⚠️ **AND IF YOU ARE UPGRADING FROM 0.7.0 - WHICH THE REFERENCE INSTALL IS -
|
|
|
|
|
THE REFUSAL THAT CAN STOP YOU IS IN 0.8.0'S SECTION, NOT IN THIS ONE.** `ha
|
|
|
|
|
addons update` does not walk the versions; it installs 0.10.0 in one step, so
|
|
|
|
|
**every validator rule added in 0.8.0 and 0.9.0 lands at the same moment as
|
|
|
|
|
0.10.0's own**, and a config that has not been looked at since 0.7.0 meets all
|
|
|
|
|
of them at once. Of those, exactly one can refuse a config nobody has edited:
|
|
|
|
|
|
|
|
|
|
> [[SAFETY-39]]'s **rule 10**, added in 0.8.0, refuses an explicit `slew_w`
|
|
|
|
|
> above `SLEW_RAIL_FRAC x` the site rail, where `SLEW_RAIL_FRAC` is
|
|
|
|
|
> `LOOP_TICK_S / PLANT_SETTLE_S` = 2/18. On a 5000 W rail that ceiling is
|
|
|
|
|
> **555.6 W** - and ⚠️ **0.7.0's own shipped default was `slew_w: 1000`**, so
|
|
|
|
|
> an install that never touched the key and simply took the default does not
|
|
|
|
|
> start on 0.10.0 until the value is lowered.
|
|
|
|
|
|
|
|
|
|
Measured on this tree: the reference blob with `slew_w: 1000` is refused, by
|
|
|
|
|
name, and the same blob at its actual `slew_w: 400` is accepted. 0.9.0's own
|
|
|
|
|
additions cannot reach a pre-0.9.0 config at all - [[OPT-09]]'s two refusals
|
|
|
|
|
fire only on a roster entry that USES `capacity_kwh`, a key that did not exist
|
|
|
|
|
- so 0.8.0's is the whole of the crossing. **Read the live options and check
|
|
|
|
|
`slew_w` against the ceiling and `price_provider` against the withdrawn list
|
|
|
|
|
BEFORE `ha addons update`**, with `tools/predeploy_check.py`, which does both.
|
|
|
|
|
|
|
|
|
|
✅ **RUN AGAINST THIS RELEASE: CLEARED, exit 0.**
|
|
|
|
|
|
|
|
|
|
docker run --rm -v <repo>:/w:ro -v <dir>:/opt:ro goodwe-addon-checks:1 \
|
|
|
|
|
python3 /w/goodwe_controller/tools/predeploy_check.py \
|
|
|
|
|
--deployed-version 0.7.0 --options /opt/live-options.json
|
|
|
|
|
|
|
|
|
|
deployed on the box : 0.7.0
|
|
|
|
|
shipped in this tree: 0.10.0
|
|
|
|
|
live options pass this version's validator
|
|
|
|
|
CLEARED for deploy.
|
|
|
|
|
|
|
|
|
|
**The option surface: 71 keys -> 75, `options:` and `schema:` in step.** Four
|
|
|
|
|
new keys, and none of them can refuse a blob written before it existed:
|
|
|
|
|
`price_formula`, `price_export_formula` and `price_taxes_profile` are `str?` -
|
|
|
|
|
**note the trailing `?`, they are OPTIONAL** - defaulting to `""`;
|
|
|
|
|
`shadow_mode` is `list(auto|on|off)` with no `?`, which is the shape that
|
|
|
|
|
becomes exit 78, and it is safe for the same reason 0.9.0's `delivery_hold_s`
|
|
|
|
|
was: it carries an `options:` default, so the Supervisor's merge supplies it to
|
|
|
|
|
every stored blob that lacks it. Nothing was removed. One default moved
|
|
|
|
|
(`price_taxes_eur_kwh`, above) and one enum widened (`price_provider` gains
|
|
|
|
|
`custom_formula`).
|
|
|
|
|
|
|
|
|
|
⚠️ **WHY 0.10.0 AND NOT 0.9.0, SINCE 0.9.0'S IMAGE NEVER REACHED THE BOX.**
|
|
|
|
|
The block this section closes used to say the choice depended on whether
|
|
|
|
|
0.9.0 had been built and pulled, and that if it had not, this work belonged
|
|
|
|
|
*inside* 0.9.0's section. That was written before 0.9.0's section was cut. It
|
|
|
|
|
has been cut since `86bdf49`, on 2026-09-07, and **28 runtime commits** have
|
|
|
|
|
landed behind it; folding them in would make the string `0.9.0` name two
|
|
|
|
|
different trees, and the Supervisor keys the built image on exactly that
|
|
|
|
|
string. Reusing it is the PKG-06/PKG-07 no-op in its worst form - a cached
|
|
|
|
|
image the Supervisor is entitled to serve while reporting success. The version
|
|
|
|
|
a tree has already shipped under is not available to be re-cut.
|
|
|
|
|
|
|
|
|
|
⚠️ **AND THIS SECTION IS NOT THE WHOLE OF WHAT 0.10.0 SHIPS. THREE TICKETS
|
|
|
|
|
WITH RUNTIME CHANGES LANDED AFTER `86bdf49` AND ARE NOT IN THE TABLE BELOW.**
|
|
|
|
|
Saying so is cheaper than moving them, and moving them is [[PKG-08]]'s card,
|
|
|
|
|
not this one:
|
|
|
|
|
|
|
|
|
|
| ticket | runtime files | where it is written down |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| [[SAFETY-43]] | `app/delivery.py`, `app/packshare.py`, `config.yaml` | the `## Unreleased` block **below `## 0.9.0`**, which is misfiled |
|
|
|
|
|
| [[TEST-21]] | `app/delivery.py`, `config.yaml`, `translations/` | the same block |
|
|
|
|
|
| [[SAFETY-44]] | `app/main.py`, `app/store.py` | ⚠️ **nowhere in this file** |
|
|
|
|
|
|
|
|
|
|
Five more landed that touch no runtime file and so cannot change what the
|
|
|
|
|
image does - PKG-13, UX-04, SAFETY-31, ARCH-02, INSTR-01. The count that
|
|
|
|
|
matters for the no-op check is `tools/predeploy_check.py`'s own: **28 runtime
|
|
|
|
|
commits since `86bdf49`**, of which the table below accounts for 21.
|
|
|
|
|
|
|
|
|
|
Contents of this release, in merge order since 0.9.0 (`86bdf49`):
|
|
|
|
|
|
|
|
|
|
| | |
|
|
|
|
|
|---|---|
|
|
|
|
|
| _this branch_ | FCST-04 - the carry-forward fill reaches the plan, and the day it carries survives a restart |
|
|
|
|
|
| _this branch_ | FCST-05 - one byte stops stopping the add-on, and the write path gets the read path's standard |
|
|
|
|
|
| _this branch_ | FCST-06 - "cannot persist" stops meaning "has no /data" and starts meaning what it says |
|
|
|
|
|
| _this branch_ | TARIFF-08 - the day-ahead feed is read in the shape it actually publishes, and the fixtures stop agreeing with the parser |
|
|
|
|
|
| _this branch_ | TARIFF-09 - the day-ahead feed is asked for the horizon it was given, so tomorrow arrives firm instead of carried |
|
|
|
|
|
| _this branch_ | TARIFF-10 - configuring a price provider is what fetches prices; trading stays its own switch |
|
|
|
|
|
| _this branch_ | TARIFF-12 - a supplier outage stops deleting the plan on its first failed fetch, and TARIFF_MAX_AGE_S starts governing |
|
|
|
|
|
| _this branch_ | TARIFF-14 - the price a trade is decided against is the price of the interval being traded |
|
|
|
|
|
| _this branch_ | TARIFF-07 - three plugins that never fetched a price are withdrawn, and a site configured on one is told so **⚠️ BACKWARD-INCOMPATIBLE** |
|
|
|
|
|
| _this branch_ | TARIFF-15 - `custom_formula`: your own tariff card's two coefficients, graded against the card's own printed price |
|
|
|
|
|
| _this branch_ | TARIFF-13 - the capacity-tariff row's "quarter(s) banked" stops resetting to 0 at every restart, and a tied peak's quarter stops moving |
|
|
|
|
|
| _this branch_ | TARIFF-11 - the add-on's account of the tariff plan stops flattering it, and the status page is graded as a surface |
|
|
|
|
|
| _this branch_ | TARIFF-17 - a price component that varies by hour has somewhere to live, so a Walloon day/night network tariff can change which hour is cheapest |
|
|
|
|
|
| _this branch_ | TARIFF-18 - `price_taxes_eur_kwh` stops shipping at half the real Flemish figure **⚠️ CHANGED DEFAULT** |
|
|
|
|
|
| _this branch_ | TARIFF-16 - the injection leg stops being one number: an injection formula with its own coefficients, its own VAT and no floor under zero |
|
|
|
|
|
| _this branch_ | SAFETY-45 - a price-driven charge is SIZED by the capacity-tariff guard instead of being switched on and off by it **⚠️ CONTROL STABILITY** |
|
|
|
|
|
| _this branch_ | SAFETY-10 - shadow mode: the whole decision path runs and nothing reaches the inverter **⚠️ NEW OPTION, DEFAULT-ON FOR A NEW INSTALL** |
|
|
|
|
|
| _this branch_ | SAFETY-47 - what a shadow run can show you, and the watts on its status row **⚠️ CORRECTS THREE CLAIMS SAFETY-10 SHIPPED** |
|
|
|
|
|
| `c2787da` | FCST-04 - the carry-forward fill reaches the plan, and the day it carries survives a restart |
|
|
|
|
|
| `5059e28` | FCST-05 - one byte stops stopping the add-on, and the write path gets the read path's standard |
|
|
|
|
|
| `958f5cd` | FCST-06 - "cannot persist" stops meaning "has no /data" and starts meaning what it says |
|
|
|
|
|
| `4257e38` | TARIFF-08 - the day-ahead feed is read in the shape it actually publishes, and the fixtures stop agreeing with the parser |
|
|
|
|
|
| `88c59a9` | TARIFF-09 - the day-ahead feed is asked for the horizon it was given, so tomorrow arrives firm instead of carried |
|
|
|
|
|
| `559f0eb` | TARIFF-10 - configuring a price provider is what fetches prices; trading stays its own switch |
|
|
|
|
|
| `a6e64fb` | TARIFF-12 - a supplier outage stops deleting the plan on its first failed fetch, and TARIFF_MAX_AGE_S starts governing |
|
|
|
|
|
| `8d09b30` | TARIFF-14 - the price a trade is decided against is the price of the interval being traded |
|
|
|
|
|
| `aade2c7` | TARIFF-07 - three plugins that never fetched a price are withdrawn, and a site configured on one is told so **⚠️ BACKWARD-INCOMPATIBLE** |
|
|
|
|
|
| `068b83f` | TARIFF-15 - `custom_formula`: your own tariff card's two coefficients, graded against the card's own printed price |
|
|
|
|
|
| `b4a3801` | TARIFF-13 - the capacity-tariff row's "quarter(s) banked" stops resetting to 0 at every restart, and a tied peak's quarter stops moving |
|
|
|
|
|
| `c518007` | TARIFF-11 - the add-on's account of the tariff plan stops flattering it, and the status page is graded as a surface |
|
|
|
|
|
| `7796f16` | TARIFF-17 - a price component that varies by hour has somewhere to live, so a Walloon day/night network tariff can change which hour is cheapest |
|
|
|
|
|
| `4dcd9b0` | TARIFF-18 - `price_taxes_eur_kwh` stops shipping at half the real Flemish figure **⚠️ CHANGED DEFAULT** |
|
|
|
|
|
| `478ba93` | TARIFF-16 - the injection leg stops being one number: an injection formula with its own coefficients, its own VAT and no floor under zero |
|
|
|
|
|
| `8f82dd4` | SAFETY-45 - a price-driven charge is SIZED by the capacity-tariff guard instead of being switched on and off by it **⚠️ CONTROL STABILITY** |
|
|
|
|
|
| `e6da478` | SAFETY-10 - shadow mode: the whole decision path runs and nothing reaches the inverter **⚠️ NEW OPTION, DEFAULT-ON FOR A NEW INSTALL** |
|
|
|
|
|
| `587495e` | SAFETY-47 - what a shadow run can show you, and the watts on its status row **⚠️ CORRECTS THREE CLAIMS SAFETY-10 SHIPPED** |
|
|
|
|
|
|
|
|
|
|
### SAFETY-47 — what a shadow run can show you, and the watts on its status row
|
|
|
|
|
|
|
|
|
@@ -1635,12 +1785,30 @@ and pulled. If it has not, this belongs inside 0.8.0's section before it ships;
|
|
|
|
|
if it has, it needs 0.8.1. Whoever deploys decides, and PKG-07 is the record of
|
|
|
|
|
what happens when nobody does.
|
|
|
|
|
|
|
|
|
|
⚠️ **ANNOTATED BY PKG-14, 2026-09-16 - EVERY ROW BELOW HAS SHIPPED, AND THE
|
|
|
|
|
PARAGRAPH ABOVE IS NO LONGER TRUE OF ANY OF THEM.** It is annotated rather
|
|
|
|
|
than rewritten, and nothing is moved out of it, because re-filing this file's
|
|
|
|
|
misplaced entries is [[PKG-08]]'s card and guessing a boundary is what that
|
|
|
|
|
card exists to stop. What is measurable, and what PKG-14 measured:
|
|
|
|
|
|
|
|
|
|
* **UX-02 shipped in 0.9.0.** Its merge `3d29993` is an ancestor of the 0.9.0
|
|
|
|
|
bump `86bdf49` and it is already listed in `## 0.9.0`'s own contents table,
|
|
|
|
|
so this row is a duplicate of a released one.
|
|
|
|
|
* **UX-04, TEST-21 and SAFETY-43 shipped in 0.10.0.** All three merged after
|
|
|
|
|
`86bdf49`. TEST-21 (`app/delivery.py`, `config.yaml`, `translations/`) and
|
|
|
|
|
SAFETY-43 (`app/delivery.py`, `app/packshare.py`, `config.yaml`) are runtime
|
|
|
|
|
changes and are named in `## 0.10.0`'s preamble as shipping there and being
|
|
|
|
|
written down here; UX-04 touches no runtime file.
|
|
|
|
|
* ⚠️ **[[SAFETY-44]] also shipped in 0.10.0 and is nowhere in this file** -
|
|
|
|
|
`app/main.py` and `app/store.py`, three commits. It is named in
|
|
|
|
|
`## 0.10.0`'s preamble for the same reason.
|
|
|
|
|
|
|
|
|
|
| | |
|
|
|
|
|
|---|---|
|
|
|
|
|
| _this branch_ | UX-02 - a configuration page in the ingress panel, where the validator's refusals can be seen |
|
|
|
|
|
| _this branch_ | UX-04 - UX-01's three unmet acceptance criteria: one withdrawn, one built, one re-attributed |
|
|
|
|
|
| _this branch_ | TEST-21 - `delivery_hold_s`'s floor re-derived from a harness that is now committed, and the recorded 30.0 s did not reproduce |
|
|
|
|
|
| _this branch_ | SAFETY-43 - delinquency measured against the ask as well as against a flat 500 W, closing OPT-09's disclosed detection regression |
|
|
|
|
|
| `3d29993` | UX-02 - a configuration page in the ingress panel, where the validator's refusals can be seen **⚠️ SHIPPED IN 0.9.0, see that section** |
|
|
|
|
|
| `bb46916` | UX-04 - UX-01's three unmet acceptance criteria: one withdrawn, one built, one re-attributed **⚠️ SHIPPED IN 0.10.0** |
|
|
|
|
|
| `6719bd8` | TEST-21 - `delivery_hold_s`'s floor re-derived from a harness that is now committed, and the recorded 30.0 s did not reproduce **⚠️ SHIPPED IN 0.10.0** |
|
|
|
|
|
| `37dd0f0` | SAFETY-43 - delinquency measured against the ask as well as against a flat 500 W, closing OPT-09's disclosed detection regression **⚠️ SHIPPED IN 0.10.0** |
|
|
|
|
|
|
|
|
|
|
⚠️ **SAFETY-43 CLOSES THE DETECTION REGRESSION OPT-09 DISCLOSED, AND IT IS A
|
|
|
|
|
TWO-INVERTER FIX. N=1 IS BYTE-IDENTICAL.** `delivery.delinquent()` named a unit
|
|
|
|
|