Compare commits

..
9 Commits
Author SHA1 Message Date
admin 3b7d6b7f05 Deploy 0.18.0 to master (#193) 2026-10-16 00:42:54 +02:00
admin 1b6f16dba2 Deploy 0.17.0 to master (#186) 2026-10-11 01:37:01 +02:00
admin df154a26d1 Deploy 0.16.0 to master (#179) 2026-10-08 01:43:37 +02:00
admin 21599938ea Merge pull request 'Deploy 0.15.0 to master' (#174) from deploy-0.15.0 into master
Deploy 0.15.0 to master (#174)
2026-10-05 10:17:38 +02:00
admin 3a54b253cb Merge pull request 'Deploy 0.14.0 to master' (#170) from deploy-0.14.0 into master
Deploy 0.14.0 to master (#170)
2026-10-03 12:32:16 +02:00
admin 8b9680fc35 Merge pull request 'Deploy 0.13.0 to master' (#166) from deploy-0.13.0 into master
Reviewed-on: #166
2026-10-02 15:26:46 +02:00
admin 3ce18708b6 PKG-18: ship 0.12.0 to master, so TEL-12's pv_entities can reach an installed 0.11.0 (#118) 2026-09-24 23:22:34 +02:00
adminandClaude Opus 5 78acb558ff PKG-17: ship 0.11.0 to master, so SAFETY-56's wording fix and TEL-11's pv_entity can reach an installed 0.10.1 (#115)
master is what the Home Assistant Supervisor clones, and the Supervisor
compares config.yaml version strings - so 0.11.0 could not be offered to an
installed 0.10.1 without this bump reaching master.

Carries SAFETY-56 (#112), TEL-11 (#113), the CI-9 follow-up (#102) and SEC-2's
docs/RELEASE.md (#108), by way of the 0.11.0 cut (#114).

NOT A FAST-FORWARD. master was not an ancestor of release/1.0: 0f14c2a, the
pre-SEC-2 direct push, exists only on master. Its tree is the same object as
the merge base b0b24e5 (c1fef26), so it carries no content delta, and
`git merge-tree --write-tree` over the two branches returns a tree bit-identical
to cccb2fa's own (6670502). This merge commit therefore contributes exactly
release/1.0's tree - master synthesises nothing.

Reviewed as the release decision, not a re-review of content. Verified from the
remote: slug goodwe_controller and every identity key unchanged (cross-checked
against the live box's Supervisor export), version 0.10.1 -> 0.11.0, and the
green is this PR's own - run 828 / job 1953, path checks.yml@refs/pull/115/head,
PRNUM 115, GRADED refs/pull/115/head merged into master, run_attempt 1, 31 test
files all passing in 8m01s of real work.

predeploy_check.py re-run against the owner's current options (exported
2026-09-24 20:02, 75 keys, pv_entity unset): CLEARED, exit 0. That retires the
stale-2026-09-07-export warning in the pull request body. Option surface
75 -> 76 with only pv_entity added, str? defaulting to "", no key removed, no
default moved, no rule added to validate.py - no SystemExit(78) on this hop.

Merging is not installing. No tag is cut here and `ha store reload` /
`ha addons update` have not been run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-24 20:28:35 +02:00
glenn schrooyenandClaude Opus 5 0f14c2a5de PKG-16: ship 0.10.1 to master, so the Supervisor can offer SAFETY-57's fix
master is what Home Assistant clones. SAFETY-57 fixed a deadlock that parked
the reference install's battery for ~13 minutes while the site exported
2.4 kW, and the Supervisor compares config.yaml version strings - so the fix
could not reach an installed 0.10.0 without this bump reaching master.

Brings SAFETY-57 (#104) and the 0.10.1 cut (#105). master was a strict
ancestor; no conflict was possible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-22 15:54:29 +02:00
7 changed files with 18 additions and 95 deletions
+3 -4
View File
@@ -3113,16 +3113,15 @@ second set of 23- and 25-hour-day bugs.
### Static day/night and fully-fixed tariffs (`static_profile`)
Most Belgian households are **not** on a dynamic contract, and an EMS that only
works under dynamic pricing is an EMS for a minority. `static_profile` is how a
works under hourly pricing is an EMS for a minority. `static_profile` is how a
two-period day/night tariff or a single fixed price becomes a first-class
configuration rather than "turn the price feature off and hope".
On the add-on page, choose **Other time windows** under Settings, Tariff and
prices, or in Setup step 4. Add one row per time window, with its start, end,
days and price. The page names any overlap, gap or empty price before you can
save. Prices take five decimals: divide the c€/kWh on your bill by 100, so
34.512 c€/kWh is 0.34512 €/kWh. You only need the JSON below for days the page
cannot choose, such as Monday and Wednesday only.
save. You only need the JSON below for days the page cannot choose, such as
Monday and Wednesday only.
It is one option, holding a JSON object, on one line:
+1 -1
View File
@@ -311,7 +311,7 @@
const days = h("select", { class: "sel", "aria-label": say(P.days) });
for (const [v, label] of f.days || []) days.append(h("option", { value: v, selected: v === row.days }, label));
days.addEventListener("change", () => set("days", days.value));
const price = h("input", { class: "num", type: "number", inputmode: "decimal", step: String(f.step || 0.01),
const price = h("input", { class: "num", type: "number", inputmode: "decimal", step: "0.01",
value: row.price == null ? "" : row.price, "aria-label": say(P.price) });
price.addEventListener("input", () => set("price", price.value === "" || isNaN(+price.value) ? null : +price.value));
const cell = (label, ...kids) => h("label", { class: "pf" }, h("span", { class: "tiny muted" }, label), ...kids);
+5 -5
View File
@@ -23,13 +23,13 @@
"contract": {"name": "Your contract", "description": "How your supplier prices each kWh you take from the grid."},
"region": {"name": "Region", "description": "Fills in grid costs, levies and VAT for the year shown, but not yet for Wallonia or Brussels, so check your bill."},
"fallback": {"name": "Price when the feed is down", "description": "Used when the price source does not answer."},
"fixed_price": {"name": "Price per kWh, all in", "description": "Energy, supplier costs, grid costs, levies and VAT together: divide c€/kWh from the bill by 100, so 34.512 c€/kWh is 0.34512."},
"day_price": {"name": "Day price, weekdays 07:00 to 22:00", "description": "All in, as on your bill: divide c€/kWh by 100, so 34.512 c€/kWh is 0.34512."},
"night_price": {"name": "Night and weekend price", "description": "All in, as on your bill: divide c€/kWh by 100, so 34.512 c€/kWh is 0.34512."},
"periods": {"name": "Time windows and prices", "description": "One price per kWh for each time window, all in, as on your bill: divide c€/kWh by 100, so 34.512 c€/kWh is 0.34512."}
"fixed_price": {"name": "Price per kWh, all in", "description": "Energy, supplier costs, grid costs, levies and VAT together."},
"day_price": {"name": "Day price, weekdays 07:00 to 22:00", "description": "All in, as on your bill."},
"night_price": {"name": "Night and weekend price", "description": "All in, as on your bill."},
"periods": {"name": "Time windows and prices", "description": "One price per kWh for each time window, all in, as on your bill."}
},
"choices": {
"contract": {"none": "No prices", "fixed": "Fixed price", "daynight": "Day and night", "dynamic": "Dynamic, every 15 minutes", "custom": "Other time windows"},
"contract": {"none": "No prices", "fixed": "Fixed price", "daynight": "Day and night", "dynamic": "Dynamic, hourly", "custom": "Other time windows"},
"fallback": {"none": "None", "fixed": "One price", "daynight": "Day and night", "custom": "Other time windows"},
"region": {"flanders": "Flanders, Fluvius Antwerpen", "own": "I enter my own figure"},
"meter_source": {"off": "One grid power entity", "ha_dsmr": "Import and export entities", "mqtt_p1": "Meter over MQTT", "ha_signed": "One net power entity", "homewizard_local": "HomeWizard meter on the network"},
+5 -5
View File
@@ -23,13 +23,13 @@
"contract": {"name": "Uw contract", "description": "Hoe uw leverancier elke kWh prijst die u van het net afneemt."},
"region": {"name": "Regio", "description": "Vult nettarieven, heffingen en btw in voor het getoonde jaar, nog niet voor Wallonië of Brussel, dus kijk uw factuur na."},
"fallback": {"name": "Prijs als de feed wegvalt", "description": "Gebruikt als de prijsbron niet antwoordt."},
"fixed_price": {"name": "Prijs per kWh, alles inbegrepen", "description": "Energie, kosten van de leverancier, nettarieven, heffingen en btw samen: deel c€/kWh van de factuur door 100, dus 34,512 c€/kWh is 0,34512."},
"day_price": {"name": "Dagprijs, weekdagen 07:00 tot 22:00", "description": "Alles inbegrepen, zoals op uw factuur: deel c€/kWh door 100, dus 34,512 c€/kWh is 0,34512."},
"night_price": {"name": "Nacht- en weekendprijs", "description": "Alles inbegrepen, zoals op uw factuur: deel c€/kWh door 100, dus 34,512 c€/kWh is 0,34512."},
"periods": {"name": "Tijdvakken en prijzen", "description": "Eén prijs per kWh voor elk tijdvak, alles inbegrepen, zoals op uw factuur: deel c€/kWh door 100, dus 34,512 c€/kWh is 0,34512."}
"fixed_price": {"name": "Prijs per kWh, alles inbegrepen", "description": "Energie, kosten van de leverancier, nettarieven, heffingen en btw samen."},
"day_price": {"name": "Dagprijs, weekdagen 07:00 tot 22:00", "description": "Alles inbegrepen, zoals op uw factuur."},
"night_price": {"name": "Nacht- en weekendprijs", "description": "Alles inbegrepen, zoals op uw factuur."},
"periods": {"name": "Tijdvakken en prijzen", "description": "Eén prijs per kWh voor elk tijdvak, alles inbegrepen, zoals op uw factuur."}
},
"choices": {
"contract": {"none": "Geen prijzen", "fixed": "Vaste prijs", "daynight": "Dag en nacht", "dynamic": "Dynamisch, per kwartier", "custom": "Andere tijdvakken"},
"contract": {"none": "Geen prijzen", "fixed": "Vaste prijs", "daynight": "Dag en nacht", "dynamic": "Dynamisch, per uur", "custom": "Andere tijdvakken"},
"fallback": {"none": "Geen", "fixed": "Eén prijs", "daynight": "Dag en nacht", "custom": "Andere tijdvakken"},
"region": {"flanders": "Vlaanderen, Fluvius Antwerpen", "own": "Ik vul zelf een bedrag in"},
"meter_source": {"off": "Eén entiteit voor netvermogen", "ha_dsmr": "Entiteiten voor afname en injectie", "mqtt_p1": "Meter via MQTT", "ha_signed": "Eén entiteit voor nettovermogen", "homewizard_local": "HomeWizard-meter op het netwerk"},
+4 -4
View File
@@ -164,13 +164,13 @@ GROUPS = (
wizard=4),
Field("price_export_formula", TEXT),
Field("ui.fallback", SEGMENTED, show=_DYNAMIC, wizard=4),
Field("ui.fixed_price", NUMBER, "€/kWh", step=0.00001,
Field("ui.fixed_price", NUMBER, "€/kWh", step=0.01,
show=(("ui.fallback", "==", "fixed"),), wizard=4),
Field("ui.day_price", NUMBER, "€/kWh", step=0.00001,
Field("ui.day_price", NUMBER, "€/kWh", step=0.01,
show=(("ui.fallback", "==", "daynight"),), wizard=4),
Field("ui.night_price", NUMBER, "€/kWh", step=0.00001,
Field("ui.night_price", NUMBER, "€/kWh", step=0.01,
show=(("ui.fallback", "==", "daynight"),), wizard=4),
Field("ui.periods", PERIOD_LIST, "€/kWh", step=0.00001,
Field("ui.periods", PERIOD_LIST, "€/kWh", step=0.01,
show=(("ui.fallback", "==", "custom"),), wizard=4),
)),
Part("selling", (
-16
View File
@@ -286,22 +286,6 @@ check("app/uitext.py falls back to English per key and reads nl-BE as nl",
and uitext._merge({"a": {"b": 1, "c": 2}}, {"a": {"b": 3}})
== {"a": {"b": 3, "c": 2}})
# ============================================================================ #
print("\n7. the dynamic contract choice says 15 minutes, never hourly (UX-44)")
# Belgian day-ahead prices are per 15 minutes since 2025-10-01.
HOURLY = re.compile(r"hourly|per hour|\bhour\b|per uur|\buur\b|uurprijs",
re.I)
_dyn = {"en": EN["choices"]["contract"]["dynamic"],
"nl": NL["choices"]["contract"]["dynamic"]}
check(f"the label reads 'Dynamic, every 15 minutes' / 'Dynamisch, per "
f"kwartier' ({_dyn})",
_dyn == {"en": "Dynamic, every 15 minutes",
"nl": "Dynamisch, per kwartier"})
check("neither language calls the dynamic contract hourly",
not any(HOURLY.search(t) for t in _dyn.values()))
check("mutation: the old labels are caught",
all(HOURLY.search(t) for t in ("Dynamic, hourly", "Dynamisch, per uur")))
print()
if fails:
print(f"{len(fails)} FAILED")
-60
View File
@@ -327,66 +327,6 @@ check("days the editor has no choice for stay in the Configuration tab, and "
["ui.periods"] is None and edit(dict(SITE, static_profile=_raw), gain=0.5)
["static_profile"] == _raw)
print("\n8. UX-43: tariff prices take five decimals")
from app import uilayout as _L # noqa: E402
_PX = 0.34512
_TARIFF = ("ui.fixed_price", "ui.day_price", "ui.night_price", "ui.periods")
_steps = {k: _L.field(k).step for k in _TARIFF}
check("the four tariff price fields step 0.00001 EUR/kWh",
sorted(_steps) == sorted(_TARIFF)
and all(v == 0.00001 for v in _steps.values()))
_f5 = edit(DEFAULTS, ui__contract="fixed", ui__fixed_price=_PX)
check("fixed price 0.34512 is saved exactly and read back exactly",
'"eur_per_kwh": 0.34512}' in _f5["static_profile"]
and U.options_to_ui(_f5)["ui.fixed_price"] == _PX
and trip(_f5) == _f5)
_d5 = edit(SITE, ui__contract="daynight", ui__day_price=_PX,
ui__night_price=0.26789)
check("day price 0.34512 and night price 0.26789 are saved and read back exactly",
'"eur_per_kwh": 0.34512' in _d5["static_profile"]
and '"eur_per_kwh": 0.26789' in _d5["static_profile"]
and U.options_to_ui(_d5)["ui.day_price"] == _PX
and U.options_to_ui(_d5)["ui.night_price"] == 0.26789
and not validate_startup_config(_d5))
_n5 = edit(SITE, ui__contract="daynight", ui__day_price=0.3, ui__night_price=_PX)
check("night price 0.34512 alone is saved and read back exactly",
U.options_to_ui(_n5)["ui.night_price"] == _PX)
_r3 = rows(("00:00", "07:00", "mon-sun", 0.12345), ("07:00", "22:00", "mon-sun", _PX),
("22:00", "24:00", "mon-sun", 0.05001))
_p3 = periods(_r3)
check("three windows with five-decimal prices save and read back exactly",
[p.eur_per_kwh for p in parse_static_profile(_p3["static_profile"]).periods]
== [0.12345, _PX, 0.05001]
and [r["price"] for r in U.options_to_ui(_p3)["ui.periods"]]
== [0.12345, _PX, 0.05001] and trip(_p3) == _p3
and "e-" not in _p3["static_profile"])
check("a two-decimal price still works", U.options_to_ui(
edit(DEFAULTS, ui__contract="fixed", ui__fixed_price=0.31))
["ui.fixed_price"] == 0.31)
_old = dict(SITE, price_provider="off", static_profile=json.dumps({"periods": [
{"name": "day", "from": "07:00", "to": "22:00", "days": "mon-fri",
"eur_per_kwh": 0.34},
{"name": "night", "from": "22:00", "to": "07:00", "days": "mon-fri",
"eur_per_kwh": 0.26},
{"name": "night", "from": "00:00", "to": "24:00", "days": "sat,sun",
"eur_per_kwh": 0.26}]}))
check("a stored profile with two-decimal prices is unchanged by load and save",
trip(_old) == _old)
_js = open(os.path.join(HERE, "app", "static", "settings.js"),
encoding="utf-8").read()
check("the window editor's price input takes its step from the field, not a "
"hard-coded 0.01", 'step: "0.01"' not in _js
and "step: String(f.step || 0.01)" in _js)
for _lang in ("en", "nl"):
with open(os.path.join(HERE, "app", "ui_text", f"{_lang}.json"),
encoding="utf-8") as _fh:
_flds = json.load(_fh)["fields"]
_hint = "34.512" if _lang == "en" else "34,512"
_eq = "0.34512" if _lang == "en" else "0,34512"
check(f"[{_lang}] the c€/kWh hint sits on all four tariff price fields",
all(_hint in _flds[k]["description"] and _eq in _flds[k]["description"]
for k in ("fixed_price", "day_price", "night_price", "periods")))
print()
if fails:
print(f"{len(fails)} FAILED")