Repository navigation
fix(config): read float-declared keys with float defaults (#4925) - #4938
Conversation
|
Not sure about this, shouldn't we fix the caller for get_arg()? |
|
(Posted by Rik; written by Claude.) Fair challenge, and normally I would agree — fix the caller, not the accessor. Two things pushed me the other way here. "The caller" is not one place. Auditing main: 15 float-declared
The original issue proposed normalising in More to the point, it only holds while every future caller keeps getting it right. The trap resets every time someone writes The framing that convinced me: the type is already declared authoritatively in There is also direct precedent — #4441 fixed this exact mechanism on the Two honest costs, so they are on the record rather than buried:
If you would rather keep the accessor dumb, the alternative I would want is all six call sites fixed plus a test that fails when a new int default appears against a float-declared key — the sweep in |
|
(Posted by Rik; written by Claude.) @springfall2008 following up — my reply above (2026-09-10) laid out the case for fixing this in |
00e9266 to
95049fc
Compare
|
(Written by Claude, posted by @chalfontchubby.) @springfall2008 I've switched this to your suggestion: the A correction to my earlier reply: it said six call sites, but there are eight - I had missed To stop a ninth appearing, there is a guard test that scans the source for any |
get_arg() coerces its return value on the type of the default it is
handed, and applies that to whatever value was resolved - real configured
value or not. So get_arg("solcast_poll_hours", 8) ran a configured 4.8
through int(float(value)) and returned 4, shortening the Solcast poll TTL
from 4.8h to 4h and pushing a two-site hobbyist account past its 10
poll/day quota into nightly HTTP 429s. Nothing warned: validate_config()
checks the raw apps.yaml value against the float schema and passes it.
Eight call sites read an APPS_SCHEMA float key with an int default: five
COMPONENT_LIST arg specs (solcast_poll_hours, forecast_solar_max_age,
open_meteo_forecast_max_age, alphaess_api_delay, axle_pence_per_kwh) and
three direct reads in octopus.py (octopus_saving_session_rate twice,
octopus_saving_session_min_octopoints_per_kwh). Each now has a float
literal. A configured alphaess_api_delay of 0.5, for example, no longer
becomes 0 and switches the AlphaESS throttle off.
Fixed at the callers rather than in get_arg(), as the maintainer asked on
review: the accessor stays keyed on the default, and a guard test stops
the callers regressing instead. It scans the source for any get_arg() call or COMPONENT_LIST spec that
gives a float-declared key an int literal default, and fails naming each
one (it lists all eight against main). A behavioural sweep also resolves
each float-declared COMPONENT_LIST spec that has a default the way
Components.initialize() does, and checks a configured fraction survives.
User-visible, cosmetic only: a whole-number value now shows with ".0" -
the Axle scheduled-event notification ("100.0 p/kWh"), the web UI's
component config table, the Axle event attribute, Octopus saving-session
octopoints_per_kwh when no reward is reported, and one Octopus log line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95049fc to
15cb16f
Compare
springfall2008
left a comment
There was a problem hiding this comment.
Approved. The callers now pass float defaults and get_arg() is unchanged, as asked for earlier. I checked every read of the 15 float-declared settings on current main: all are covered, and nothing downstream needs an integer. Both new tests passed on a local merge with main before the rebase.
One nit for a follow-up, not a blocker: test_component_arg_specs_resolve_float_declared_keys_as_float restores with my_predbat.args = original_args, which rebinds to a copy rather than restoring the shared dict in place.
(Review written by Claude on behalf of @springfall2008.)
…); mark GH#4925 fixed in PR #4938; note the PR #4738 time-entity escape hatch (#5453) Folds (each verified against main c073c36 before landing): - GH#5437 -> "Too many API errors" symptom row: one HA outage walks both mechanisms (REST counter + #5134's init failure); bare ValueError => empty str(e); fatal_error init-once, never cleared; full-log discriminator (Startup banners vs none); wrapper lives in predbat_addon (out of reach). - GH#5438 -> Octopus row: vehicle-catalogue name matching keeps the last match; device attrs override configured rate/battery size outright (fetch.py "Take the max"); coordinator string-drop warning as marker. - GH#5442 -> curve row: id == 0 caller gate in execute.py (both branches, lineage #683); find_charge_curve is per-inverter ("Inverter N Looking for" discriminator); one fleet-wide curve, not per-inverter suffixable; auto-mode re-run signature; debug-yaml double-args trap. - GH#5444 -> Ohme row: no-active-session 404 class vs #4719 route withdrawal; unguarded control_charge() => 60s pause retry storm + health degrade; /config/ohme.py path trap; FINISHED excluded from CONNECTED_STATUSES gates the car plan (link marked unverified). - GH#5445 -> new Excluded-load row: car_charging_energy is the generic excluded-load list; car_energy_reported_load is the inside-clamp flag (four consumer sites); no heat-pump key/diagram node; predheat row cross-ref. - GH#5446 -> HA write/verify row: flag-vs-wiring contradiction (capability flag false silently discards apps.yaml wiring; user_configured_entity() protects only the four time entities); Wrote-X-successfully on a sensor.* placeholder is a self-echo, control_ledger ties log line to it. - GH#5450 -> Savings row: restore/accumulate block, no plausibility guard, recorder-history fallback covers attribute reads; start_date = first #2844-run day, not a reset; recovery via state edit + re-stating start_date; #1114/#3108 archaeology ruled out as inflators. - GH#5452 -> Compare row: standing charge excluded from compare (enable_standing_charge omits "compare" since #1952); compare config: blocks only override CONFIG_ITEMS; cost10 save-flip breaks plan HTML; Actual-vs-projection standing-charge gap; stale annual.py comments. Re-check vs merged main: - GH#4925 fixed in PR #4938 (merged 2026-10-08): entry updated, mechanism kept for older logs. - PR #4738 (merged 2026-10-08) added user_configured_entity(): #5214 entry updated (time entities only) and GH#5446 extension added. - Savings row's drifted line cites re-anchored on symbols. Co-authored-by: CI <ci@example.com> Co-authored-by: Claude Code <noreply@anthropic.com>
Fixes #4925.
Problem
get_arg()coerces its return value on the type of the default it is handed, and applies that to whatever value was resolved - real configured value or not. Soget_arg("solcast_poll_hours", 8)ran a configured4.8throughint(float(value))and returned4, shortening the Solcast poll TTL from 4.8h to 4h and pushing a two-site hobbyist account past its 10 poll/day quota into nightly HTTP 429s. Nothing warned:validate_config()checks the raw apps.yaml value against the float schema and passes it.Fix
Float literals at every call site that reads an
APPS_SCHEMAfloat-declared key with an int default - eight of them:components.pyspecsolcast_poll_hourscomponents.pyspecforecast_solar_max_agecomponents.pyspecopen_meteo_forecast_max_agecomponents.pyspecalphaess_api_delaycomponents.pyspecaxle_pence_per_kwhoctopus.pyoctopus_saving_session_rate(twice)octopus.pyoctopus_saving_session_min_octopoints_per_kwhThe issue's own suggestion (normalise in
Components.initialize()) would miss the three directoctopus.pyreads. A configuredalphaess_api_delayof 0.5, for example, no longer becomes 0 and switches the AlphaESS throttle off.This fixes the callers rather than
get_arg(), as asked on review: the accessor stays keyed on its default, and a guard test stops the callers regressing instead.User-visible, cosmetic only: a whole-number value now shows with ".0" - the Axle scheduled-event notification ("100.0 p/kWh"), the web UI's component config table, the Axle event attribute, Octopus saving-session
octopoints_per_kwhwhen no reward is reported, and one Octopus log line.Not included: the issue's second ask, a warning on lossy coercion. It would fire every cycle on keys that are integer by design, and the guard below prevents the float-key case at source.
Testing
test_float_declared_keys_never_read_with_int_defaultscans the source for anyget_arg()call orCOMPONENT_LISTspec giving a float-declared key an int literal default, and fails naming each. Against main it names all eight. It sees literals only - a default passed through a variable, or a wrapper accessor, is not checked.test_component_arg_specs_resolve_float_declared_keys_as_floatresolves each float-declared spec that has a default the wayComponents.initialize()does, and checks a configured 4.8 survives (5 specs). Fails against main'scomponents.py../run_all --quickandpre-commit run --all-filespass (exit 0).Written by Claude on behalf of @chalfontchubby.
🤖 Generated with Claude Code