PRICE-01: price_service provider, fallback chain, and the gate read from it
A new price source, `price_provider: price_service`: the owner's price server,
one fixed HTTPS host, a bearer token (new password option
`price_service_token`), wholesale energy only so markup and taxes are added as
for generic_dayahead.
The chain is server, then energy-charts, then the prices the history store
kept. A step counts only if it has a price for now. Each step down and back up
is one log line. With every source down a site plans from what it saved.
No price for now: one status row, one Home plan line and one notification, in
English and Dutch, cleared when a price returns. With this provider the
maintenance charge gate reads the provider's prices and keeps the Home
Assistant rule: the current 15-minute price is at or below today's average.
The token is hidden on the settings page, refused when missing, and taken out
of any error text. Other providers, `off` and the Home Assistant pair take the
road they always took (scripted-day probe against 0ce7d35: one line differs,
the refusal that lists the known providers). See
DECISIONS/PRICE-01-own-price-server.md.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
co-authored by
Claude
parent
0ce7d35f80
commit
9ac3a9d2bc
@@ -129,6 +129,8 @@ S_NOT_DELIVERING = "An inverter is not delivering"
|
||||
T_METER_FROZEN = "Meter reading may be stuck"
|
||||
T_MAINT_FAILED = "Battery cycle did not finish"
|
||||
T_MAINT_FAILED_NL = "Batterijcyclus niet afgerond"
|
||||
T_NO_PRICES = "No prices"
|
||||
T_NO_PRICES_NL = "Geen prijzen"
|
||||
|
||||
ID_LOOP_BOUND = "goodwe_integrator_bound"
|
||||
ID_OUTPUT_CLAMP = "goodwe_output_clamp"
|
||||
@@ -138,6 +140,7 @@ ID_CIRCULATING = "goodwe_circulating_power"
|
||||
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"
|
||||
|
||||
# Which limit cut a command (clamp.ClampResult.bound, plus the loop's own).
|
||||
# The setting is named by its label, "Inverter rating (W)".
|
||||
@@ -314,6 +317,51 @@ def maint_failed(kind: str, days: int, unit: str = "", slug: str = "") -> Notice
|
||||
T_MAINT_FAILED, en, body_nl=f"{T_MAINT_FAILED_NL}. {nl}")
|
||||
|
||||
|
||||
def since_text(since, now) -> str:
|
||||
"""PRICE-01: `00:00` for a moment today, `13 Oct at 21:00` for an earlier
|
||||
day. Both arguments are already in the site's zone. Empty for no moment."""
|
||||
if since is None:
|
||||
return ""
|
||||
return f"{since:%H:%M}" if since.date() == now.date() else when(since)
|
||||
|
||||
|
||||
def no_prices(since_text: str = "") -> Notice:
|
||||
"""PRICE-01: no source has a price for now. Said once, gone with the gap."""
|
||||
if since_text:
|
||||
en = (f"No prices since {since_text}, so the monthly battery cycle "
|
||||
f"waits until they return.")
|
||||
nl = (f"Geen prijzen sinds {since_text}, dus de maandelijkse "
|
||||
f"batterijcyclus wacht tot ze terug zijn.")
|
||||
else:
|
||||
en = ("No prices have come in yet, so the monthly battery cycle waits "
|
||||
"until they do.")
|
||||
nl = ("Er zijn nog geen prijzen binnen, dus de maandelijkse "
|
||||
"batterijcyclus wacht tot ze er zijn.")
|
||||
return Notice(ID_NO_PRICES, WARNING, T_NO_PRICES, en,
|
||||
body_nl=f"{T_NO_PRICES_NL}. {nl}")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Dismiss:
|
||||
"""The payload that takes one notification down again."""
|
||||
id: str
|
||||
|
||||
def data(self) -> dict:
|
||||
return {"notification_id": self.id}
|
||||
|
||||
|
||||
def prices_back() -> Dismiss:
|
||||
"""PRICE-01: the 'No prices' notification, taken down when a price is back."""
|
||||
return Dismiss(ID_NO_PRICES)
|
||||
|
||||
|
||||
def row_no_prices(since_text: str = "") -> str:
|
||||
"""PRICE-01: the status row while no price covers now."""
|
||||
if since_text:
|
||||
return f"No prices since {since_text}, maintenance waits."
|
||||
return "No prices yet, maintenance waits."
|
||||
|
||||
|
||||
def row_maint_waits(until_text: str) -> str:
|
||||
"""MAINT-13: the status row while a failed cycle waits."""
|
||||
return f"Waits until {until_text} after a cycle that did not finish."
|
||||
@@ -1186,6 +1234,9 @@ def status_samples() -> list:
|
||||
True)),
|
||||
("row_circulating_check", GREEN, row_circulating_check(0, 0, 60)),
|
||||
("row_maint_waits", AMBER, row_maint_waits("2026-10-13")),
|
||||
("row_no_prices", AMBER, row_no_prices("00:00")),
|
||||
("row_no_prices", AMBER, row_no_prices("13 Oct at 21:00")),
|
||||
("row_no_prices", AMBER, row_no_prices()),
|
||||
]
|
||||
|
||||
|
||||
@@ -1218,6 +1269,9 @@ def samples() -> list:
|
||||
meter_frozen(90),
|
||||
maint_failed("charge", 7),
|
||||
maint_failed("drain", 7, "inverter-b", "inverter_b"),
|
||||
no_prices("00:00"),
|
||||
no_prices("13 Oct at 21:00"),
|
||||
no_prices(),
|
||||
]
|
||||
|
||||
|
||||
@@ -1250,6 +1304,7 @@ LABELS = {
|
||||
"price_formula": "Your own tariff formula",
|
||||
"price_export_formula": "Your own injection formula",
|
||||
"price_csv_path": "Price CSV file",
|
||||
"price_service_token": "Price service token",
|
||||
"price_now_entity": "Current price entity",
|
||||
"price_avg_entity": "Average price entity",
|
||||
"arbitrage_enabled": "Trade on the price spread",
|
||||
|
||||
Reference in New Issue
Block a user