PRICE-01 round 3: replace the fixed-tariff notification with a log line and a settings note
G1: remove the false 'Fixed tariff not used' notification. One INFO line at start and an info note beside the setting (EN/NL) when price_service has a non-blank static_profile; the old notification id is dismissed at start. DOCS, migration step 5 and DECISIONS rule 7 now say what the code does. S05: whitespace-only static_profile counts as not set. G2: maintenance_enabled read through MaintConfig.from_options(). Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
co-authored by
Claude
parent
2f261f8ba4
commit
c68fd275cc
@@ -90,7 +90,7 @@ from .p1 import P1Ingest, build_source, is_enabled
|
||||
from .price import Absence, Uncertainty, brussels, price_option, provider_configured
|
||||
from .price import required_spread as price_required_spread
|
||||
from .price import uncertainty_of as price_uncertainty_of
|
||||
from .price_chain import (PriceWatch, fixed_tariff_unused, gate_for,
|
||||
from .price_chain import (PriceWatch, fixed_tariff_beside_service, gate_for,
|
||||
uses_chain)
|
||||
from .price_history import PriceHistory
|
||||
from .static_tariff import resolve_tariff, static_gated, static_status
|
||||
@@ -1214,7 +1214,7 @@ class Controller:
|
||||
self.price_gap = False
|
||||
self.price_gap_since = None
|
||||
self._price_gap_announced = False
|
||||
self._fixed_unused_said = False
|
||||
self._fixed_note_said = False
|
||||
self._gated_key = None # MAINT-13: (options, hour) of the cache
|
||||
# TARIFF-03. One line of prose for the status page saying WHICH tariff
|
||||
# window we are in and why it is or is not cheap - so "Cheap window:
|
||||
@@ -4035,7 +4035,7 @@ class Controller:
|
||||
# Owner, 2026-10-14: the words say 'maintenance waits', so they show
|
||||
# only while the maintenance charge is on.
|
||||
self.price_gap = ((not gate.covered) and self.tariff_at is not None
|
||||
and self.o.get("maintenance_enabled") is True)
|
||||
and MaintConfig.from_options(self.o).enabled)
|
||||
last = self._last_published
|
||||
if gate.covered or last is None:
|
||||
self.price_gap_since = None
|
||||
@@ -5591,7 +5591,7 @@ class Controller:
|
||||
# Not a safety check, so it is one of the cheap ones: an attribute test
|
||||
# and, on a change only, one Home Assistant call.
|
||||
await self.check_price_gap()
|
||||
await self.check_fixed_unused()
|
||||
await self.check_fixed_note()
|
||||
|
||||
# --- SAFETY-31's alarm, and its boundary is the LATCH, not a number ---
|
||||
# Unconstrained where the two above are not: it grades
|
||||
@@ -6229,18 +6229,21 @@ class Controller:
|
||||
await self.hass.call("persistent_notification", "dismiss",
|
||||
messages.prices_back().data())
|
||||
|
||||
async def check_fixed_unused(self) -> None:
|
||||
"""PRICE-01 F2. A `static_profile` beside `price_service` no longer
|
||||
drives the cheap window or the maintenance gate. Not refused: said once,
|
||||
at the first cycle, in the log and as one notification."""
|
||||
if self._fixed_unused_said:
|
||||
async def check_fixed_note(self) -> None:
|
||||
"""PRICE-01 G1. At the first cycle only: one INFO line when a
|
||||
`static_profile` is set beside `price_service` (it is the plan's last
|
||||
fallback, not a fault, so no notification), and a dismiss of the card an
|
||||
earlier build raised, which is harmless when absent."""
|
||||
if self._fixed_note_said:
|
||||
return
|
||||
self._fixed_unused_said = True
|
||||
if fixed_tariff_unused(self.o):
|
||||
_LOG.warning("The fixed tariff is not used, because the price "
|
||||
"service gives the prices.")
|
||||
await self.hass.call("persistent_notification", "create",
|
||||
messages.fixed_tariff_unused().data())
|
||||
self._fixed_note_said = True
|
||||
if fixed_tariff_beside_service(self.o):
|
||||
_LOG.info(messages.FIXED_TARIFF_LOG)
|
||||
try:
|
||||
await self.hass.call("persistent_notification", "dismiss",
|
||||
messages.fixed_tariff_card_gone().data())
|
||||
except Exception: # noqa: BLE001 - housekeeping never stops a cycle
|
||||
_LOG.debug("Could not take down the old fixed-tariff card.")
|
||||
|
||||
def _gap_args(self) -> tuple:
|
||||
"""`(since, now)` in Brussels time for the gap's words. `since` is None
|
||||
|
||||
@@ -131,8 +131,6 @@ T_MAINT_FAILED = "Battery cycle did not finish"
|
||||
T_MAINT_FAILED_NL = "Batterijcyclus niet afgerond"
|
||||
T_NO_PRICES = "No prices"
|
||||
T_NO_PRICES_NL = "Prijzen ontbreken"
|
||||
T_FIXED_UNUSED = "Fixed tariff not used"
|
||||
T_FIXED_UNUSED_NL = "Vast tarief niet gebruikt"
|
||||
|
||||
ID_LOOP_BOUND = "goodwe_integrator_bound"
|
||||
ID_OUTPUT_CLAMP = "goodwe_output_clamp"
|
||||
@@ -143,7 +141,9 @@ ID_NOT_DELIVERING = "goodwe_not_delivering_{slug}"
|
||||
ID_METER_FROZEN = "goodwe_meter_frozen"
|
||||
ID_MAINT_FAILED = "goodwe_maint_failed_{slug}"
|
||||
ID_NO_PRICES = "goodwe_no_prices"
|
||||
ID_FIXED_UNUSED = "goodwe_fixed_tariff_unused"
|
||||
# PRICE-01: an earlier build raised a card with this id. Nothing raises it now;
|
||||
# the add-on only dismisses it at start.
|
||||
OLD_FIXED_CARD_ID = "goodwe_fixed_tariff_unused"
|
||||
|
||||
# Which limit cut a command (clamp.ClampResult.bound, plus the loop's own).
|
||||
# The setting is named by its label, "Inverter rating (W)".
|
||||
@@ -354,17 +354,6 @@ def no_prices(since=None, now=None) -> Notice:
|
||||
body_nl=f"{T_NO_PRICES_NL}. {nl}")
|
||||
|
||||
|
||||
def fixed_tariff_unused() -> Notice:
|
||||
"""PRICE-01: a fixed tariff is set beside the price service, which wins.
|
||||
Said once, at start."""
|
||||
return Notice(
|
||||
ID_FIXED_UNUSED, WARNING, T_FIXED_UNUSED,
|
||||
"The fixed tariff is not used, because the price service gives the "
|
||||
"prices.",
|
||||
body_nl=f"{T_FIXED_UNUSED_NL}. Het vaste tarief wordt niet gebruikt, "
|
||||
"want de prijsdienst levert de prijzen.")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Dismiss:
|
||||
"""The payload that takes one notification down again."""
|
||||
@@ -379,6 +368,22 @@ def prices_back() -> Dismiss:
|
||||
return Dismiss(ID_NO_PRICES)
|
||||
|
||||
|
||||
# PRICE-01: a fixed tariff beside the price service. One true sentence, for the
|
||||
# start-up log and the settings page (neither has a Dutch layer).
|
||||
FIXED_TARIFF_LOG = "Fixed tariff is only used if no prices are available."
|
||||
FIXED_TARIFF_NOTE = ("With the price service, the fixed tariff is only used if "
|
||||
"no prices are available.")
|
||||
FIXED_TARIFF_NOTE_NL = ("Met de prijsdienst wordt het vaste tarief alleen "
|
||||
"gebruikt als er geen prijzen zijn.")
|
||||
# Settings-page notes that have a Dutch twin (uipage.anchors).
|
||||
NOTE_NL = {FIXED_TARIFF_NOTE: FIXED_TARIFF_NOTE_NL}
|
||||
|
||||
|
||||
def fixed_tariff_card_gone() -> Dismiss:
|
||||
"""PRICE-01: takes down the card an earlier build raised."""
|
||||
return Dismiss(OLD_FIXED_CARD_ID)
|
||||
|
||||
|
||||
def row_no_prices(since=None, now=None) -> str:
|
||||
"""PRICE-01: the status row while no price covers now."""
|
||||
if since is not None:
|
||||
@@ -1301,7 +1306,6 @@ def samples() -> list:
|
||||
no_prices(*_SINCE_SAMPLE),
|
||||
no_prices(*_SINCE_OLDER),
|
||||
no_prices(),
|
||||
fixed_tariff_unused(),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -50,9 +50,10 @@ def uses_chain(opts: dict) -> bool:
|
||||
return bool(cls is not None and (cls.FALLBACKS or cls.KEEPS_LAST_GOOD))
|
||||
|
||||
|
||||
def fixed_tariff_unused(opts: dict) -> bool:
|
||||
"""PRICE-01 F2: a fixed tariff is set beside `price_service`, which then
|
||||
decides the cheap window and the gate. Accepted, so it is said, not refused."""
|
||||
def fixed_tariff_beside_service(opts: dict) -> bool:
|
||||
"""PRICE-01: a fixed tariff is set beside `price_service`. Not a fault: it
|
||||
stays the plan's last fallback (`resolve_tariff`), and only the cheap window
|
||||
and the gate ignore it. A blank or whitespace-only value is not set."""
|
||||
return (provider_id(opts) == "price_service"
|
||||
and bool(str(price_option(opts, "static_profile")).strip()))
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import copy
|
||||
import os
|
||||
import re
|
||||
|
||||
from . import messages
|
||||
from . import uilayout as L
|
||||
from . import uitext
|
||||
from . import uimodel as U
|
||||
@@ -247,7 +248,7 @@ def derive_messages(errors, problems, notes, model, keys, lang="en"):
|
||||
kept = [p for p in problems if p in kept]
|
||||
notes = [n for n in notes if not set(n.keys or ()) & failed]
|
||||
return ([derive_message(err, lang) for err in errors]
|
||||
+ anchors(kept, notes, model, keys))
|
||||
+ anchors(kept, notes, model, keys, lang))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -292,9 +293,10 @@ def targets(key: str, text: str, model: dict) -> list:
|
||||
return [(None, None)]
|
||||
|
||||
|
||||
def anchors(problems, notes, model, keys) -> list:
|
||||
def anchors(problems, notes, model, keys, lang="en") -> list:
|
||||
"""[{field, card, tier, text}] for the refusals and notes. `field` is
|
||||
None for a sentence that names no setting on the page."""
|
||||
None for a sentence that names no setting on the page. A note with a Dutch
|
||||
twin in `messages.NOTE_NL` is shown in Dutch when `lang` is "nl"."""
|
||||
out, seen = [], set()
|
||||
|
||||
def add(field, card, tier, text):
|
||||
@@ -312,10 +314,11 @@ def anchors(problems, notes, model, keys) -> list:
|
||||
add(None, None, "bad", text)
|
||||
for note in notes:
|
||||
tier = note.tier if note.tier in _TIERS else "info"
|
||||
text = messages.NOTE_NL.get(note.text, note.text) if lang == "nl" else note.text
|
||||
for key in note.keys or (None,):
|
||||
for field, card in (targets(key, note.text, model) if key
|
||||
else [(None, None)]):
|
||||
add(field, card, tier, note.text)
|
||||
add(field, card, tier, text)
|
||||
# A sentence placed on one inverter's field is not also put on the list.
|
||||
roster = _roster()
|
||||
placed = {m["text"] for m in out
|
||||
|
||||
@@ -1659,6 +1659,12 @@ def _price_problems(opts: dict, notes: list | None = None) -> list[str]:
|
||||
f"{S('static_profile')} is empty, so the site has no tariff "
|
||||
"at all while the price feed is down.")
|
||||
|
||||
# PRICE-01: not a fault, so an info note and no log line here (the add-on
|
||||
# logs it once at start). With `price_service` the fixed tariff is only the
|
||||
# plan's last fallback; the cheap window and the gate do not read it.
|
||||
if pid == "price_service" and str(opts.get("static_profile", "") or "").strip():
|
||||
_advise(notes, "info", ("static_profile",), messages.FIXED_TARIFF_NOTE)
|
||||
|
||||
# ⚠️ OPT-04. THE THREE WAYS `arbitrage_enabled: true` CAN BE A LIE, refused
|
||||
# rather than logged, because a strategy that is armed in the UI and cannot
|
||||
# fire is indistinguishable from one that fires and finds no opportunity -
|
||||
|
||||
@@ -312,7 +312,8 @@ def build_app(controller, selfcfg=None) -> web.Application:
|
||||
_keys_of(options),
|
||||
uitext.lang_of(_body.get("lang")))
|
||||
if errors
|
||||
else uipage.anchors(problems, notes, model, _keys_of(options)))
|
||||
else uipage.anchors(problems, notes, model, _keys_of(options),
|
||||
uitext.lang_of(_body.get("lang"))))
|
||||
payload["steps"] = uipage.step_problems(payload["messages"], model)
|
||||
if _body.get("setup"):
|
||||
payload["setup"] = [n._asdict() for n in await _setup_checks(options)]
|
||||
|
||||
Reference in New Issue
Block a user